feat(ui): refine sidebar workspace and code blocks

This commit is contained in:
JOJO 2026-05-28 03:53:49 +08:00
parent 348cad2b1a
commit 3bc6f718eb
12 changed files with 6280 additions and 4984 deletions

View File

@ -0,0 +1,150 @@
---
name: ui-aesthetic-design
description: 用于进行高质量 UI 美学设计、界面精致化、组件视觉评审与前端 demo 设计的通用设计规范。当任务涉及现代系统界面、侧边栏、资源面板、消息流、代码展示、按钮、hover/active 状态、二级菜单、展示项/可选项区分、内容对齐、图标视觉体积对齐、浅色/深色主题等 UI 美学问题时使用。
---
# UI Aesthetic Design
当设计、评审或实现 UI 改动时使用此 skill。目标不是做通用 SaaS 模板,而是形成克制、现代、精致、像成熟 AI 系统产品的界面。
## 核心审美方向
整体风格参考 ChatGPT / Claude / Linear 一类现代 AI 系统的克制感:
- 使用干净的浅色/深色表面、细边框、轻量阴影和安静的 hover 状态。
- 避免过度发光、重阴影、毛玻璃、强模糊、圆角矩形套圆角矩形、霓虹渐变和无意义装饰。
- 优先使用细线图标、浅灰 hover 底色、短而稳定的动效。
- 布局要紧凑但不拥挤,信息密度可以高,但不能乱。
- 精致感来自准确的间距、对齐、命中区域、状态反馈和微交互,而不是视觉噪音。
## 颜色与字体层级
使用少量语义化颜色:
- 背景/表面:浅色模式使用白色或近白;深色模式使用近黑或炭黑。
- 主文字:高对比,用于主要内容。
- 次文字:浅灰,用于路径、状态、元信息、辅助说明。
- 边框:中性 1px 细边框。
- Hover低透明度灰色填充。
- Active/当前项:略强的低透明度灰色,或明确但克制的选择标记。
- Danger只用于删除等破坏性操作。
字体层级:
- 主内容文字应更深、更大或更有字重。
- 次内容文字应更浅、更小。
- 不要为了“丰富界面”凭空添加解释性次级文本;只有真实数据结构里存在时才显示。
- 紧凑面板里的行默认单行显示,长文本截断;不要因为某条内容更长就撑高行高,除非产品信息结构明确需要多行。
## 对齐原则
追求**内容对齐**,不是容器对齐,也不是机械文字左对齐。
- 对齐有意义的视觉内容,而不是外层矩形框。
- 如果某一行由 SVG + 文字组成,标题或相关内容应根据 SVG 的视觉位置做对齐,而不是盲目对齐文字边缘。
- 如果下方具体内容本身没有图标,不要为了凑齐文字起点而乱加假图标。
- 按实际视觉体积对齐:不同 SVG 的 viewBox 和形状不同,真实尺寸可以不同,只要视觉大小一致。
- 不要强行把所有 SVG 设成同样宽高。
- 可以统一按钮命中区域,但图标本身大小要按视觉体积校准。
## 组件状态原则
状态设计必须反映真实语义:
- Hover只用于可交互项。
- Active/当前:只用于当前选择、当前模式或当前项。
- Disabled降低对比度并移除点击暗示。
- 展示内容:不要看起来像按钮;避免 hover 底色、阴影、指针光标。
- 可点击内容:可以使用轻量 hover 底色、指针光标和当前项标记。
不要把“有没有阴影”当成唯一状态区别。状态语义应该通过结构、标记、间距和交互暗示共同表达。
## 可选项 vs 展示项
设计前必须先判断内容类型。
### 可选项
用于用户从多个选项中选择一个,例如环境切换、资源位置切换、账号/空间切换、筛选条件切换。
- 必须有明确的“可选择”视觉语义,例如 radio 圆点、check 标记或清晰的选中行。
- 当前项必须一眼能看出来。
- 可以有浅灰 hover 底色。
- 行本身可以是可点击的。
- `默认`、`当前` 这类 badge 只是元信息,不能单独承担“这是可选项”的职责。
### 展示项
用于展示状态或列表数据,例如任务条目、命令摘要、进程摘要、通知摘要,除非真实组件会打开详情。
- 保持安静,不要做成按钮或卡片。
- 不要乱加假图标。
- 不要加卡片阴影或嵌套圆角容器。
- 如果真实产品里不是可点击项,就不要加 hover 效果。
- 紧凑面板中使用统一行高和单行截断。
- 只显示真实数据模型里存在的字段。
## 二级菜单与更多操作
二级菜单要一致、克制、可读:
- 使用紧凑浮层,带轻量边框、适中圆角和轻阴影。
- 菜单选项无论文字长短hover 背景宽度必须一样长。
- 菜单宽度由最长内容决定,所有选项填满同一宽度。
- 不要让每个选项的阴影/hover 背景长度随文字变短变长。
- 文案要短。
- 删除等破坏性项只用 danger 色区分,不要用更大尺寸或更重阴影。
- 列表行右侧 `...` 使用简单圆点视觉hover 可以只变色,不一定加背景。
## 侧边栏展开原则
消息类/导航类侧边栏可使用“遮罩露出”模型,而不是新建一个展开浮层:
- 把侧边栏理解为一张完整面板,被一个移动边界遮住。
- 收起态只露出固定图标列。
- 展开时右侧竖线向右移动,内容被裁切后逐渐露出。
- 每个动作从始至终只有一个图标,不要展开后再出现第二个图标。
- 图标右侧文字和新出现的列表内容同时执行两种效果:淡入 + 随移动边界逐渐露出。
- 展开时不应挤压主内容布局;如设计目标是覆盖/露出,就保持主布局稳定。
- 收起态 hover 只作用在图标的圆角正方形上,不要露出被裁切的大圆角整行背景。
- 展开态 hover 才可以作用于整行。
## 资源面板原则
资源面板、管理面板、辅助面板应避免旧式“圆角矩形套圆角矩形套圆角矩形”的低级感。
- 优先做窄而扁平的面板;如果它位于主导航旁边,可以让展开后的侧边栏完整覆盖它。
- 顶部标题显示当前内容类型,例如资源、环境、任务、命令、通知、日志等。不要把所有模式标题硬塞到同一种视觉层级里。
- 标题下方可放一个“模式切换按钮”,点击展开二级菜单;旁边按需放紧凑管理图标按钮。
- 不要把浅色小号“管理”文字按钮放在大标题同一行,容易层级违和;优先使用图标按钮。
- 不要无意义增加搜索框;只有真实工作流需要搜索或过滤时才加。
- 层级资源树可以使用文件/文件夹/类别图标,因为这类图标本身承载结构语义。
- 环境/空间/资源位置切换属于可选项 UI不是普通展示文本。
- 任务、命令、进程、通知等区域按真实数据字段显示:
- 任务/进程:标识、摘要、状态、可选当前步骤。
- 命令/作业:命令或作业预览、状态、返回码/结果码。
- 清单/待办:序号、标题、状态;如果存在总说明,可单独显示。
- 内容行默认统一高度,除非真实信息结构明确需要展开。
## 代码展示原则
代码块、日志块或等宽文本展示精致化时:
- 做成现代 artifact/card 感1px 细边框、较大圆角、清晰 header/body 分区。
- 避免粗边框和重嵌套面板。
- 复制/运行按钮要遵循同一套按钮系统。
- Header 里的语言名称是次级信息;代码内容才是主内容。
- padding、等宽字体、行高要统一流式输出时避免视觉抖动。
## 实现前检查清单
修改 UI 前必须:
1. 先读取真实组件、状态或数据结构,不准凭印象设计假数据。
2. 判断每个内容是可选项、可点击动作,还是展示内容。
3. 按视觉内容对齐,而不是按容器边缘或文字边缘机械对齐。
4. 复用产品已有图标资源或统一图标库,不要自己编造 SVG。
5. 统一图标命中区域,但按视觉体积调整真实图标大小。
6. 避免嵌套卡片、大阴影和假的 hover 暗示。
7. 同时检查浅色和深色模式。
8. 写 demo 时也要贴近真实产品结构和真实字段;如必须使用示例数据,要保持字段语义真实,不要写臆想说明。

200
docs/code_block_demo.html Normal file
View File

@ -0,0 +1,200 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Code Block Demo</title>
<style>
:root {
color-scheme: light;
--bg: #ffffff;
--text: #111111;
--muted: #6f6f6f;
--surface: #ffffff;
--border: rgba(0, 0, 0, 0.105);
--hover: rgba(0, 0, 0, 0.055);
--radius: 20px;
--mono: "SF Mono", "JetBrains Mono", "Menlo", "Monaco", "Consolas", monospace;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
:root[data-theme="dark"] {
color-scheme: dark;
--bg: #1a1a1a;
--text: #f3f3f3;
--muted: #9b9b9b;
--surface: #1a1a1a;
--border: rgba(255, 255, 255, 0.13);
--hover: rgba(255, 255, 255, 0.08);
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background: var(--bg);
color: var(--text);
display: flex;
justify-content: center;
padding: 48px 24px;
}
button { font: inherit; }
.page { width: min(960px, 100%); }
.topbar { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 24px; }
.title { margin: 0; font-size: 30px; line-height: 1.12; letter-spacing: -0.045em; font-weight: 680; }
.subtitle { margin: 6px 0 0; color: var(--muted); font-size: 14px; line-height: 1.35; }
.theme-toggle { height: 36px; border: 1px solid var(--border); border-radius: 999px; background: transparent; color: var(--text); padding: 0 13px; cursor: pointer; }
.theme-toggle:hover { background: var(--hover); }
.message { max-width: 860px; font-size: 16px; line-height: 1.65; }
.message p { margin: 0 0 14px; }
.code-block {
margin: 16px 0 28px;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--surface);
overflow: hidden;
}
.code-top {
height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 0 12px 0 18px;
background: var(--surface);
border-bottom: 1px solid var(--border);
}
.code-lang {
color: var(--muted);
font-size: 14px;
line-height: 1;
letter-spacing: -0.01em;
font-weight: 500;
}
.copy-btn {
width: 32px;
height: 32px;
border: 0;
border-radius: 10px;
background: transparent;
color: var(--muted);
display: grid;
place-items: center;
cursor: pointer;
transition: background 140ms ease, color 140ms ease;
}
.copy-btn:hover {
background: var(--hover);
color: var(--text);
}
.icon {
width: 18px;
height: 18px;
display: inline-block;
background: currentColor;
mask: var(--icon-url) center / contain no-repeat;
-webkit-mask: var(--icon-url) center / contain no-repeat;
}
.icon-copy { --icon-url: url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%2024%2024%22%20width=%2248%22%20height=%2248%22%20fill=%22none%22%20stroke=%22%23000000%22%20stroke-width=%222%22%20stroke-linecap=%22round%22%20stroke-linejoin=%22round%22%20style=%22opacity:1;%22%3E%3Crect%20width=%2214%22%20height=%2214%22%20x=%228%22%20y=%228%22%20rx=%222%22%20ry=%222%22/%3E%3Cpath%20d=%22M4%2016c-1.1%200-2-.9-2-2V4c0-1.1.9-2%202-2h10c1.1%200%202%20.9%202%202%22/%3E%3C/svg%3E"); }
.icon-check { --icon-url: url("data:image/svg+xml,%3Csvg%0A%20%20xmlns=%22http://www.w3.org/2000/svg%22%0A%20%20width=%2224%22%0A%20%20height=%2224%22%0A%20%20viewBox=%220%200%2024%2024%22%0A%20%20fill=%22none%22%0A%20%20stroke=%22currentColor%22%0A%20%20stroke-width=%222%22%0A%20%20stroke-linecap=%22round%22%0A%20%20stroke-linejoin=%22round%22%0A%3E%0A%20%20%3Cpath%20d=%22M20%206%209%2017l-5-5%22%20/%3E%0A%3C/svg%3E"); }
pre {
margin: 0;
padding: 24px 28px 28px;
background: var(--surface);
overflow: auto;
font-family: var(--mono);
font-size: 15px;
line-height: 1.64;
tab-size: 4;
}
code { font-family: inherit; color: var(--text); white-space: pre; }
.kw { color: #c13f7a; }
.fn { color: #6a45c2; }
.str { color: #08863f; }
.num { color: #8b5e00; }
.op { color: #c13f7a; }
:root[data-theme="dark"] .kw { color: #ff7ab2; }
:root[data-theme="dark"] .fn { color: #b69cff; }
:root[data-theme="dark"] .str { color: #5fd38d; }
:root[data-theme="dark"] .num { color: #e4b86a; }
</style>
</head>
<body>
<main class="page">
<header class="topbar">
<div>
<h1 class="title">代码块 Demo</h1>
<p class="subtitle">顶部更矮;顶部和代码区同色;只用一条细线分割;不添加不存在的功能。</p>
</div>
<button class="theme-toggle" id="themeToggle" type="button">深色模式</button>
</header>
<section class="message">
<p>示例代码块:</p>
<div class="code-block">
<div class="code-top">
<span class="code-lang">Python</span>
<button class="copy-btn" type="button" title="复制代码" aria-label="复制代码" data-copy="pythonCode">
<span class="icon icon-copy" aria-hidden="true"></span>
</button>
</div>
<pre><code id="pythonCode"><span class="kw">import</span> random
<span class="kw">from</span> datetime <span class="kw">import</span> datetime
quotes <span class="op">=</span> [
<span class="str">"Stay curious."</span>,
<span class="str">"Code is poetry."</span>,
<span class="str">"Small steps every day."</span>,
<span class="str">"Debugging is detective work."</span>,
]
<span class="kw">def</span> <span class="fn">random_quote</span>():
now <span class="op">=</span> datetime.now().strftime(<span class="str">"%Y-%m-%d %H:%M:%S"</span>)
quote <span class="op">=</span> random.choice(quotes)
<span class="kw">return</span> <span class="str">f"[{now}] {quote}"</span>
<span class="kw">if</span> __name__ <span class="op">==</span> <span class="str">"__main__"</span>:
<span class="kw">for</span> i <span class="kw">in</span> range(<span class="num">3</span>):
print(random_quote())</code></pre>
</div>
<div class="code-block">
<div class="code-top">
<span class="code-lang">powershell</span>
<button class="copy-btn" type="button" title="复制代码" aria-label="复制代码" data-copy="psCode">
<span class="icon icon-copy" aria-hidden="true"></span>
</button>
</div>
<pre><code id="psCode">Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser -Force</code></pre>
</div>
</section>
</main>
<script>
const root = document.documentElement;
const themeToggle = document.getElementById('themeToggle');
themeToggle.addEventListener('click', () => {
const next = root.dataset.theme === 'dark' ? 'light' : 'dark';
root.dataset.theme = next;
themeToggle.textContent = next === 'dark' ? '浅色模式' : '深色模式';
});
document.querySelectorAll('.copy-btn').forEach((button) => {
button.addEventListener('click', () => {
// file:// 预览下不调用剪贴板 API避免浏览器安全报错这里只演示点击后的视觉反馈。
const icon = button.querySelector('.icon');
icon.className = 'icon icon-check';
setTimeout(() => { icon.className = 'icon icon-copy'; }, 1000);
});
});
</script>
</body>
</html>

View File

@ -0,0 +1,581 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Sidebar Reveal Demo</title>
<style>
:root {
color-scheme: light;
--bg: #ffffff;
--sidebar-bg: #fbfbfb;
--text: #111111;
--muted: #8f8f8f;
--border: rgba(0, 0, 0, 0.09);
--hover: rgba(0, 0, 0, 0.055);
--hover-strong: rgba(0, 0, 0, 0.095);
--active: rgba(0, 0, 0, 0.078);
--menu-bg: #ffffff;
--menu-shadow: 0 12px 28px rgba(0, 0, 0, 0.14);
--collapsed-width: 52px;
--expanded-width: 306px;
--icon-cell: 40px;
--row-height: 40px;
--row-radius: 12px;
--anim: 320ms cubic-bezier(0.22, 1, 0.36, 1);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
:root[data-theme="dark"] {
color-scheme: dark;
--bg: #1a1a1a;
--sidebar-bg: #0f0f0f;
--text: #f4f4f4;
--muted: #8f8f8f;
--border: rgba(255, 255, 255, 0.105);
--hover: rgba(255, 255, 255, 0.078);
--hover-strong: rgba(255, 255, 255, 0.13);
--active: rgba(255, 255, 255, 0.105);
--menu-bg: #202020;
--menu-shadow: 0 18px 36px rgba(0, 0, 0, 0.42);
}
* { box-sizing: border-box; }
body {
margin: 0;
height: 100vh;
background: var(--bg);
color: var(--text);
overflow: hidden;
}
button,
input {
font: inherit;
}
.app {
position: relative;
height: 100vh;
background: var(--bg);
}
.main {
min-width: 0;
height: 100vh;
margin-left: var(--collapsed-width);
display: flex;
align-items: flex-start;
justify-content: center;
padding: 28px 32px;
}
.main-title {
margin: 0;
font-size: 34px;
line-height: 1.1;
font-weight: 620;
letter-spacing: -0.04em;
}
.main-title span {
color: var(--muted);
font-weight: 430;
}
.sidebar-frame {
position: fixed;
left: 0;
top: 0;
z-index: 50;
width: var(--collapsed-width);
height: 100vh;
overflow: hidden;
background: var(--sidebar-bg);
border-right: 1px solid var(--border);
transition: width var(--anim), background 160ms ease, border-color 160ms ease;
will-change: width;
}
.sidebar-frame.open {
width: var(--expanded-width);
}
.sidebar-content {
width: var(--expanded-width);
height: 100%;
background: var(--sidebar-bg);
display: flex;
flex-direction: column;
padding: 18px 8px 14px 6px;
transition: background 160ms ease;
}
.sidebar-top {
display: flex;
flex-direction: column;
gap: 2px;
flex: 0 0 auto;
}
.sidebar-bottom {
margin-top: 8px;
flex: 0 0 auto;
}
.side-row,
.search-shell,
.conversation-row {
width: 100%;
min-height: var(--row-height);
border: 0;
border-radius: var(--row-radius);
background: transparent;
color: var(--text);
display: grid;
grid-template-columns: var(--icon-cell) minmax(0, 1fr);
align-items: center;
padding: 0;
text-align: left;
}
.side-row,
.conversation-row {
cursor: pointer;
transition: background 140ms ease, color 140ms ease;
}
.side-row:hover,
.conversation-row:hover {
background: transparent;
}
.side-icon {
width: var(--icon-cell);
height: var(--row-height);
border-radius: var(--row-radius);
display: grid;
place-items: center;
color: currentColor;
flex: 0 0 auto;
transition: background 140ms ease;
}
.sidebar-frame:not(.open) .side-row:hover .side-icon,
.sidebar-frame:not(.open) .theme-toggle:hover .side-icon,
.sidebar-frame:not(.open) .conversation-row:hover .side-icon {
background: var(--hover);
}
.sidebar-frame.open .side-row:hover,
.sidebar-frame.open .side-row.active,
.sidebar-frame.open .conversation-row:hover,
.sidebar-frame.open .conversation-row.menu-open {
background: var(--hover);
}
.side-icon svg {
display: block;
stroke: currentColor;
flex: 0 0 auto;
}
/* 保留视觉校准:按钮容器统一,图标真实尺寸不机械统一 */
.side-icon--chat svg { width: 29px; height: 29px; }
.side-icon--pencil svg { width: 22px; height: 22px; }
.side-icon--workspace svg { width: 23px; height: 23px; }
.side-icon--monitor svg { width: 23px; height: 23px; }
.side-icon--theme svg { width: 22px; height: 22px; }
.side-icon--user svg { width: 22px; height: 22px; }
.side-label,
.search-area,
.conversation-list,
.conversation-title,
.conversation-more {
opacity: 0;
transition: opacity 220ms ease;
transition-delay: 0ms;
}
.sidebar-frame.open .side-label,
.sidebar-frame.open .search-area,
.sidebar-frame.open .conversation-list,
.sidebar-frame.open .conversation-title,
.sidebar-frame.open .conversation-more {
opacity: 1;
transition-delay: 80ms;
}
.side-label {
min-width: 0;
padding-left: 8px;
padding-right: 12px;
font-size: 15px;
line-height: 1;
letter-spacing: -0.015em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.search-area {
flex: 0 0 auto;
margin-top: 10px;
}
.search-shell {
height: var(--row-height);
overflow: hidden;
transition: background 140ms ease;
}
.search-shell:focus-within,
.search-shell:hover {
background: var(--hover);
}
.search-input {
min-width: 0;
width: calc(var(--expanded-width) - var(--icon-cell) - 22px);
height: var(--row-height);
border: 0;
outline: 0;
padding: 0 12px 0 8px;
background: transparent;
color: var(--text);
font-size: 14px;
letter-spacing: -0.01em;
}
.search-input::placeholder {
color: var(--muted);
}
.conversation-list {
min-height: 0;
flex: 1 1 auto;
overflow-y: auto;
padding-top: 12px;
padding-right: 0;
scrollbar-width: thin;
}
.conversation-row {
position: relative;
grid-template-columns: minmax(0, 1fr) 40px;
min-height: var(--row-height);
margin: 0;
}
.conversation-title {
min-width: 0;
padding-left: 12px;
padding-right: 4px;
color: var(--text);
font-size: 14px;
line-height: 1.25;
letter-spacing: -0.01em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.conversation-more {
width: 30px;
height: 30px;
border: 0;
border-radius: 9px;
background: transparent;
color: var(--muted);
display: grid;
place-items: center;
cursor: pointer;
justify-self: start;
visibility: hidden;
transition: background 140ms ease, color 140ms ease, opacity 160ms ease;
}
.conversation-row:hover .conversation-more,
.conversation-row.menu-open .conversation-more {
visibility: visible;
}
.conversation-more:hover,
.conversation-row.menu-open .conversation-more {
background: transparent;
color: var(--text);
}
.conversation-more svg {
width: 22px;
height: 22px;
stroke: currentColor;
}
.context-menu {
position: fixed;
z-index: 80;
width: max-content;
min-width: 92px;
padding: 6px;
border: 1px solid var(--border);
border-radius: 14px;
background: var(--menu-bg);
box-shadow: var(--menu-shadow);
opacity: 0;
transform: translateY(-4px) scale(0.98);
pointer-events: none;
transition:
opacity 120ms ease,
transform 150ms cubic-bezier(0.22, 1, 0.36, 1),
background 160ms ease,
border-color 160ms ease;
}
.context-menu.open {
opacity: 1;
transform: translateY(0) scale(1);
pointer-events: auto;
}
.menu-button {
width: max-content;
min-width: 80px;
height: 36px;
border: 0;
border-radius: 10px;
background: transparent;
color: var(--text);
display: grid;
grid-template-columns: 24px auto;
align-items: center;
padding: 0 10px 0 8px;
text-align: left;
cursor: pointer;
font-size: 14px;
letter-spacing: -0.01em;
transition: background 140ms ease;
}
.menu-button:hover {
background: var(--hover);
}
.menu-button.danger {
color: #d6483b;
}
:root[data-theme="dark"] .menu-button.danger {
color: #ff7b72;
}
.menu-button svg {
width: 18px;
height: 18px;
stroke: currentColor;
}
.demo-hint {
position: fixed;
left: calc(var(--collapsed-width) + 20px);
bottom: 18px;
color: var(--muted);
font-size: 13px;
pointer-events: none;
}
</style>
</head>
<body>
<div class="app">
<aside class="sidebar-frame" id="sidebar" aria-label="对话侧边栏">
<div class="sidebar-content">
<div class="sidebar-top">
<button class="side-row" id="toggleSidebar" type="button" aria-label="展开对话记录" aria-expanded="false">
<span class="side-icon side-icon--chat" aria-hidden="true">
<svg viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 6.5c0-1.38 1.12-2.5 2.5-2.5h13c1.38 0 2.5 1.12 2.5 2.5v8.5c0 1.38-1.12 2.5-2.5 2.5h-5.6l-3.4 3.2.6-3.2H7.5c-1.38 0-2.5-1.12-2.5-2.5V6.5z" stroke-width="1.45" stroke-linejoin="round" />
<path d="M9 9.5h10" stroke-width="1.45" stroke-linecap="round" />
<path d="M9 13h6" stroke-width="1.45" stroke-linecap="round" />
</svg>
</span>
<span class="side-label">对话记录</span>
</button>
<button class="side-row" type="button" aria-label="新建对话">
<span class="side-icon side-icon--pencil" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
<path d="m15 5 4 4" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</span>
<span class="side-label">新建对话</span>
</button>
<button class="side-row" type="button" aria-label="工作区">
<span class="side-icon side-icon--workspace" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 3 4.5 7.15v9.7L12 21l7.5-4.15v-9.7L12 3Z" stroke-width="1.55" stroke-linejoin="round" />
<path d="M4.8 7.35 12 11.5l7.2-4.15M12 11.5V21" stroke-width="1.55" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</span>
<span class="side-label">工作区</span>
</button>
<button class="side-row" type="button" aria-label="虚拟显示器">
<span class="side-icon side-icon--monitor" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="3.5" y="5" width="17" height="12" rx="2" stroke-width="1.55" />
<path d="M9 21h6M12 17v4" stroke-width="1.55" stroke-linecap="round" />
</svg>
</span>
<span class="side-label">虚拟显示器</span>
</button>
</div>
<div class="search-area" aria-label="搜索对话">
<label class="search-shell">
<span class="side-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="width:22px;height:22px;stroke:currentColor">
<circle cx="11" cy="11" r="7" stroke-width="1.55" />
<path d="m20 20-3.4-3.4" stroke-width="1.55" stroke-linecap="round" />
</svg>
</span>
<input class="search-input" type="search" placeholder="搜索对话" />
</label>
</div>
<div class="conversation-list" aria-label="对话记录">
<div class="conversation-row" data-title="随意的 py 代码块">
<span class="conversation-title">随意的 py 代码块</span>
<button class="conversation-more" type="button" aria-label="更多操作">
<svg viewBox="0 0 24 24" fill="none"><path d="M6 12h.01M12 12h.01M18 12h.01" stroke-width="3" stroke-linecap="round" /></svg>
</button>
</div>
<div class="conversation-row" data-title="前端现代化设计讨论">
<span class="conversation-title">前端现代化设计讨论</span>
<button class="conversation-more" type="button" aria-label="更多操作">
<svg viewBox="0 0 24 24" fill="none"><path d="M6 12h.01M12 12h.01M18 12h.01" stroke-width="3" stroke-linecap="round" /></svg>
</button>
</div>
<div class="conversation-row" data-title="审批面板覆盖窗口方案">
<span class="conversation-title">审批面板覆盖窗口方案</span>
<button class="conversation-more" type="button" aria-label="更多操作">
<svg viewBox="0 0 24 24" fill="none"><path d="M6 12h.01M12 12h.01M18 12h.01" stroke-width="3" stroke-linecap="round" /></svg>
</button>
</div>
<div class="conversation-row" data-title="CLI 显示层细节修复">
<span class="conversation-title">CLI 显示层细节修复</span>
<button class="conversation-more" type="button" aria-label="更多操作">
<svg viewBox="0 0 24 24" fill="none"><path d="M6 12h.01M12 12h.01M18 12h.01" stroke-width="3" stroke-linecap="round" /></svg>
</button>
</div>
<div class="conversation-row" data-title="个人空间设置页重设计">
<span class="conversation-title">个人空间设置页重设计</span>
<button class="conversation-more" type="button" aria-label="更多操作">
<svg viewBox="0 0 24 24" fill="none"><path d="M6 12h.01M12 12h.01M18 12h.01" stroke-width="3" stroke-linecap="round" /></svg>
</button>
</div>
</div>
<div class="sidebar-bottom">
<button class="side-row theme-toggle" id="themeToggle" type="button" aria-label="切换深色模式">
<span class="side-icon side-icon--theme" aria-hidden="true">
<svg class="theme-icon theme-icon-light" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="4" stroke-width="1.55" />
<path d="M12 2.8v2.3M12 18.9v2.3M4.2 4.2l1.6 1.6M18.2 18.2l1.6 1.6M2.8 12h2.3M18.9 12h2.3M4.2 19.8l1.6-1.6M18.2 5.8l1.6-1.6" stroke-width="1.55" stroke-linecap="round" />
</svg>
</span>
<span class="side-label">浅色模式</span>
</button>
<button class="side-row" type="button" aria-label="个人空间">
<span class="side-icon side-icon--user" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="8" r="4" stroke-width="1.6" />
<path d="M4.5 21a7.5 7.5 0 0 1 15 0" stroke-width="1.6" stroke-linecap="round" />
</svg>
</span>
<span class="side-label">个人空间</span>
</button>
</div>
</div>
</aside>
<main class="main">
<h1 class="main-title">AI Agent <span>Auto</span></h1>
</main>
<div class="context-menu" id="contextMenu" role="menu" aria-hidden="true">
<button class="menu-button" type="button" role="menuitem">
<svg viewBox="0 0 24 24" fill="none"><rect x="8" y="8" width="11" height="11" rx="2" stroke-width="1.55"/><path d="M5 15V7a2 2 0 0 1 2-2h8" stroke-width="1.55" stroke-linecap="round"/></svg>
<span>复制</span>
</button>
<button class="menu-button danger" type="button" role="menuitem">
<svg viewBox="0 0 24 24" fill="none"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 14h10l1-14M9 7V4h6v3" stroke-width="1.55" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span>删除</span>
</button>
</div>
</div>
<div class="demo-hint">点击第一个按钮折叠/露出;对话行 hover 后显示 ...;点击 ... 打开二级菜单。</div>
<script>
const sidebar = document.getElementById('sidebar');
const toggle = document.getElementById('toggleSidebar');
const themeToggle = document.getElementById('themeToggle');
const contextMenu = document.getElementById('contextMenu');
let menuOwner = null;
toggle.addEventListener('click', () => {
const isOpen = sidebar.classList.toggle('open');
toggle.setAttribute('aria-expanded', String(isOpen));
toggle.setAttribute('aria-label', isOpen ? '收起对话记录' : '展开对话记录');
if (!isOpen) closeMenu();
});
themeToggle.addEventListener('click', () => {
const root = document.documentElement;
const next = root.dataset.theme === 'dark' ? 'light' : 'dark';
root.dataset.theme = next;
themeToggle.querySelector('.side-label').textContent = next === 'dark' ? '深色模式' : '浅色模式';
themeToggle.setAttribute('aria-label', next === 'dark' ? '切换浅色模式' : '切换深色模式');
});
function closeMenu() {
contextMenu.classList.remove('open');
contextMenu.setAttribute('aria-hidden', 'true');
if (menuOwner) menuOwner.classList.remove('menu-open');
menuOwner = null;
}
document.querySelectorAll('.conversation-more').forEach((button) => {
button.addEventListener('click', (event) => {
event.stopPropagation();
const row = button.closest('.conversation-row');
if (menuOwner === row && contextMenu.classList.contains('open')) {
closeMenu();
return;
}
closeMenu();
menuOwner = row;
row.classList.add('menu-open');
const rect = button.getBoundingClientRect();
contextMenu.style.left = '0px';
contextMenu.style.top = `${rect.bottom + 6}px`;
contextMenu.classList.add('open');
contextMenu.setAttribute('aria-hidden', 'false');
const menuWidth = contextMenu.offsetWidth;
contextMenu.style.left = `${Math.min(rect.right - menuWidth, window.innerWidth - menuWidth - 12)}px`;
});
});
document.addEventListener('click', (event) => {
if (!contextMenu.contains(event.target)) closeMenu();
});
window.addEventListener('resize', closeMenu);
</script>
</body>
</html>

View File

@ -0,0 +1,556 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Sidebar + Workspace Demo</title>
<style>
:root {
color-scheme: light;
--bg: #ffffff;
--panel-bg: #fbfbfb;
--text: #111111;
--muted: #8f8f8f;
--border: rgba(0, 0, 0, 0.09);
--hover: rgba(0, 0, 0, 0.055);
--hover-strong: rgba(0, 0, 0, 0.095);
--menu-bg: #ffffff;
--menu-shadow: 0 12px 28px rgba(0, 0, 0, 0.14);
--collapsed-width: 52px;
--expanded-width: 306px;
--workspace-width: calc(var(--expanded-width) - var(--collapsed-width));
--icon-cell: 40px;
--row-height: 40px;
--row-radius: 12px;
--anim: 320ms cubic-bezier(0.22, 1, 0.36, 1);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
:root[data-theme="dark"] {
color-scheme: dark;
--bg: #1a1a1a;
--panel-bg: #0f0f0f;
--text: #f4f4f4;
--muted: #8f8f8f;
--border: rgba(255, 255, 255, 0.105);
--hover: rgba(255, 255, 255, 0.078);
--hover-strong: rgba(255, 255, 255, 0.13);
--menu-bg: #202020;
--menu-shadow: 0 18px 36px rgba(0, 0, 0, 0.42);
}
* { box-sizing: border-box; }
body { margin: 0; height: 100vh; background: var(--bg); color: var(--text); overflow: hidden; }
button, input { font: inherit; }
button { -webkit-tap-highlight-color: transparent; }
.icon {
display: inline-block;
width: var(--icon-size, 20px);
height: var(--icon-size, 20px);
background: currentColor;
mask: var(--icon-url) center / contain no-repeat;
-webkit-mask: var(--icon-url) center / contain no-repeat;
flex: 0 0 auto;
}
.icon-chat { --icon-url: url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%2032%2032%22%20fill=%22none%22%20stroke=%22currentColor%22%20stroke-width=%222.2%22%20stroke-linecap=%22round%22%20stroke-linejoin=%22round%22%3E%0A%20%20%3Cpath%20d=%22M6%207c0-2%201.6-3.6%203.6-3.6h13c2%200%203.6%201.6%203.6%203.6v10c0%202-1.6%203.6-3.6%203.6h-6.2L12.4%2025l0.7-4.4H9.6c-2%200-3.6-1.6-3.6-3.6V7Z%22%20/%3E%0A%20%20%3Cpath%20d=%22M10.4%2011h11.2%22%20/%3E%0A%20%20%3Cpath%20d=%22M10.4%2015h7.2%22%20/%3E%0A%3C/svg%3E"); --icon-size: 29px; }
.icon-pencil { --icon-url: url("data:image/svg+xml,%3Csvg%0A%20%20xmlns=%22http://www.w3.org/2000/svg%22%0A%20%20width=%2224%22%0A%20%20height=%2224%22%0A%20%20viewBox=%220%200%2024%2024%22%0A%20%20fill=%22none%22%0A%20%20stroke=%22currentColor%22%0A%20%20stroke-width=%222%22%0A%20%20stroke-linecap=%22round%22%0A%20%20stroke-linejoin=%22round%22%0A%3E%0A%20%20%3Cpath%20d=%22M21.174%206.812a1%201%200%200%200-3.986-3.987L3.842%2016.174a2%202%200%200%200-.5.83l-1.321%204.352a.5.5%200%200%200%20.623.622l4.353-1.32a2%202%200%200%200%20.83-.497z%22%20/%3E%0A%20%20%3Cpath%20d=%22m15%205%204%204%22%20/%3E%0A%3C/svg%3E"); --icon-size: 22px; }
.icon-layers { --icon-url: url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%2024%2024%22%20width=%2248%22%20height=%2248%22%20fill=%22none%22%20stroke=%22#000000%22%20stroke-width=%222%22%20stroke-linecap=%22round%22%20stroke-linejoin=%22round%22%20style=%22opacity:1;%22%3E%3Cpath%20d=%22M12.83%202.18a2%202%200%200%200-1.66%200L2.6%206.08a1%201%200%200%200%200%201.83l8.58%203.91a2%202%200%200%200%201.66%200l8.58-3.9a1%201%200%200%200%200-1.83z%22/%3E%3Cpath%20d=%22M2%2012a1%201%200%200%200%20.58.91l8.6%203.91a2%202%200%200%200%201.65%200l8.58-3.9A1%201%200%200%200%2022%2012%22/%3E%3Cpath%20d=%22M2%2017a1%201%200%200%200%20.58.91l8.6%203.91a2%202%200%200%200%201.65%200l8.58-3.9A1%201%200%200%200%2022%2017%22/%3E%3C/svg%3E"); --icon-size: 23px; }
.icon-monitor { --icon-url: url("data:image/svg+xml,%3Csvg%0A%20%20xmlns=%22http://www.w3.org/2000/svg%22%0A%20%20width=%2224%22%0A%20%20height=%2224%22%0A%20%20viewBox=%220%200%2024%2024%22%0A%20%20fill=%22none%22%0A%20%20stroke=%22currentColor%22%0A%20%20stroke-width=%222%22%0A%20%20stroke-linecap=%22round%22%0A%20%20stroke-linejoin=%22round%22%0A%3E%0A%20%20%3Crect%20width=%2220%22%20height=%2214%22%20x=%222%22%20y=%223%22%20rx=%222%22%20/%3E%0A%20%20%3Cline%20x1=%228%22%20x2=%2216%22%20y1=%2221%22%20y2=%2221%22%20/%3E%0A%20%20%3Cline%20x1=%2212%22%20x2=%2212%22%20y1=%2217%22%20y2=%2221%22%20/%3E%0A%3C/svg%3E"); --icon-size: 23px; }
.icon-search { --icon-url: url("data:image/svg+xml,%3Csvg%0A%20%20xmlns=%22http://www.w3.org/2000/svg%22%0A%20%20width=%2224%22%0A%20%20height=%2224%22%0A%20%20viewBox=%220%200%2024%2024%22%0A%20%20fill=%22none%22%0A%20%20stroke=%22currentColor%22%0A%20%20stroke-width=%222%22%0A%20%20stroke-linecap=%22round%22%0A%20%20stroke-linejoin=%22round%22%0A%3E%0A%20%20%3Cpath%20d=%22m21%2021-4.34-4.34%22%20/%3E%0A%20%20%3Ccircle%20cx=%2211%22%20cy=%2211%22%20r=%228%22%20/%3E%0A%3C/svg%3E"); --icon-size: 22px; }
.icon-user { --icon-url: url("data:image/svg+xml,%3Csvg%0A%20%20xmlns=%22http://www.w3.org/2000/svg%22%0A%20%20width=%2224%22%0A%20%20height=%2224%22%0A%20%20viewBox=%220%200%2024%2024%22%0A%20%20fill=%22none%22%0A%20%20stroke=%22currentColor%22%0A%20%20stroke-width=%222%22%0A%20%20stroke-linecap=%22round%22%0A%20%20stroke-linejoin=%22round%22%0A%3E%0A%20%20%3Cpath%20d=%22M19%2021v-2a4%204%200%200%200-4-4H9a4%204%200%200%200-4%204v2%22%20/%3E%0A%20%20%3Ccircle%20cx=%2212%22%20cy=%227%22%20r=%224%22%20/%3E%0A%3C/svg%3E"); --icon-size: 22px; }
.icon-folder { --icon-url: url("data:image/svg+xml,%3Csvg%0A%20%20xmlns=%22http://www.w3.org/2000/svg%22%0A%20%20width=%2224%22%0A%20%20height=%2224%22%0A%20%20viewBox=%220%200%2024%2024%22%0A%20%20fill=%22none%22%0A%20%20stroke=%22currentColor%22%0A%20%20stroke-width=%222%22%0A%20%20stroke-linecap=%22round%22%0A%20%20stroke-linejoin=%22round%22%0A%3E%0A%20%20%3Cpath%20d=%22M20%2020a2%202%200%200%200%202-2V8a2%202%200%200%200-2-2h-7.9a2%202%200%200%201-1.69-.9L9.6%203.9A2%202%200%200%200%207.93%203H4a2%202%200%200%200-2%202v13a2%202%200%200%200%202%202Z%22%20/%3E%0A%3C/svg%3E"); --icon-size: 18px; }
.icon-folder-open { --icon-url: url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%2024%2024%22%20width=%2248%22%20height=%2248%22%20fill=%22none%22%20stroke=%22#000000%22%20stroke-width=%222%22%20stroke-linecap=%22round%22%20stroke-linejoin=%22round%22%20style=%22opacity:1;%22%3E%3Cpath%20%20%20%20%20%20d=%22m6%2014l1.5-2.9A2%202%200%200%201%209.24%2010H20a2%202%200%200%201%201.94%202.5l-1.54%206a2%202%200%200%201-1.95%201.5H4a2%202%200%200%201-2-2V5a2%202%200%200%201%202-2h3.9a2%202%200%200%201%201.69.9l.81%201.2a2%202%200%200%200%201.67.9H18a2%202%200%200%201%202%202v2%22/%3E%3C/svg%3E"); --icon-size: 18px; }
.icon-file { --icon-url: url("data:image/svg+xml,%3Csvg%0A%20%20xmlns=%22http://www.w3.org/2000/svg%22%0A%20%20width=%2224%22%0A%20%20height=%2224%22%0A%20%20viewBox=%220%200%2024%2024%22%0A%20%20fill=%22none%22%0A%20%20stroke=%22currentColor%22%0A%20%20stroke-width=%222%22%0A%20%20stroke-linecap=%22round%22%0A%20%20stroke-linejoin=%22round%22%0A%3E%0A%20%20%3Cpath%20d=%22M6%2022a2%202%200%200%201-2-2V4a2%202%200%200%201%202-2h8a2.4%202.4%200%200%201%201.704.706l3.588%203.588A2.4%202.4%200%200%201%2020%208v12a2%202%200%200%201-2%202z%22%20/%3E%0A%20%20%3Cpath%20d=%22M14%202v5a1%201%200%200%200%201%201h5%22%20/%3E%0A%3C/svg%3E"); --icon-size: 17px; }
.icon-bot { --icon-url: url("data:image/svg+xml,%3Csvg%0A%20%20xmlns=%22http://www.w3.org/2000/svg%22%0A%20%20width=%2224%22%0A%20%20height=%2224%22%0A%20%20viewBox=%220%200%2024%2024%22%0A%20%20fill=%22none%22%0A%20%20stroke=%22currentColor%22%0A%20%20stroke-width=%222%22%0A%20%20stroke-linecap=%22round%22%0A%20%20stroke-linejoin=%22round%22%0A%3E%0A%20%20%3Cpath%20d=%22M12%208V4H8%22%20/%3E%0A%20%20%3Crect%20width=%2216%22%20height=%2212%22%20x=%224%22%20y=%228%22%20rx=%222%22%20/%3E%0A%20%20%3Cpath%20d=%22M2%2014h2%22%20/%3E%0A%20%20%3Cpath%20d=%22M20%2014h2%22%20/%3E%0A%20%20%3Cpath%20d=%22M15%2013v2%22%20/%3E%0A%20%20%3Cpath%20d=%22M9%2013v2%22%20/%3E%0A%3C/svg%3E"); --icon-size: 18px; }
.icon-terminal { --icon-url: url("data:image/svg+xml,%3Csvg%0A%20%20xmlns=%22http://www.w3.org/2000/svg%22%0A%20%20width=%2224%22%0A%20%20height=%2224%22%0A%20%20viewBox=%220%200%2024%2024%22%0A%20%20fill=%22none%22%0A%20%20stroke=%22currentColor%22%0A%20%20stroke-width=%222%22%0A%20%20stroke-linecap=%22round%22%0A%20%20stroke-linejoin=%22round%22%0A%3E%0A%20%20%3Cpath%20d=%22M12%2019h8%22%20/%3E%0A%20%20%3Cpath%20d=%22m4%2017%206-6-6-6%22%20/%3E%0A%3C/svg%3E"); --icon-size: 18px; }
.icon-sticky { --icon-url: url("data:image/svg+xml,%3Csvg%0A%20%20xmlns=%22http://www.w3.org/2000/svg%22%0A%20%20width=%2224%22%0A%20%20height=%2224%22%0A%20%20viewBox=%220%200%2024%2024%22%0A%20%20fill=%22none%22%0A%20%20stroke=%22currentColor%22%0A%20%20stroke-width=%222%22%0A%20%20stroke-linecap=%22round%22%0A%20%20stroke-linejoin=%22round%22%0A%3E%0A%20%20%3Cpath%20d=%22M21%209a2.4%202.4%200%200%200-.706-1.706l-3.588-3.588A2.4%202.4%200%200%200%2015%203H5a2%202%200%200%200-2%202v14a2%202%200%200%200%202%202h14a2%202%200%200%200%202-2z%22%20/%3E%0A%20%20%3Cpath%20d=%22M15%203v5a1%201%200%200%200%201%201h5%22%20/%3E%0A%3C/svg%3E"); --icon-size: 18px; }
.icon-check { --icon-url: url("data:image/svg+xml,%3Csvg%0A%20%20xmlns=%22http://www.w3.org/2000/svg%22%0A%20%20width=%2224%22%0A%20%20height=%2224%22%0A%20%20viewBox=%220%200%2024%2024%22%0A%20%20fill=%22none%22%0A%20%20stroke=%22currentColor%22%0A%20%20stroke-width=%222%22%0A%20%20stroke-linecap=%22round%22%0A%20%20stroke-linejoin=%22round%22%0A%3E%0A%20%20%3Cpath%20d=%22M20%206%209%2017l-5-5%22%20/%3E%0A%3C/svg%3E"); --icon-size: 17px; }
.icon-clock { --icon-url: url("data:image/svg+xml,%3Csvg%0A%20%20xmlns=%22http://www.w3.org/2000/svg%22%0A%20%20width=%2224%22%0A%20%20height=%2224%22%0A%20%20viewBox=%220%200%2024%2024%22%0A%20%20fill=%22none%22%0A%20%20stroke=%22currentColor%22%0A%20%20stroke-width=%222%22%0A%20%20stroke-linecap=%22round%22%0A%20%20stroke-linejoin=%22round%22%0A%3E%0A%20%20%3Ccircle%20cx=%2212%22%20cy=%2212%22%20r=%229%22%20/%3E%0A%20%20%3Cpath%20d=%22M12%207v5l3%203%22%20/%3E%0A%3C/svg%3E"); --icon-size: 17px; }
.icon-laptop { --icon-url: url("data:image/svg+xml,%3Csvg%0A%20%20xmlns=%22http://www.w3.org/2000/svg%22%0A%20%20width=%2224%22%0A%20%20height=%2224%22%0A%20%20viewBox=%220%200%2024%2024%22%0A%20%20fill=%22none%22%0A%20%20stroke=%22currentColor%22%0A%20%20stroke-width=%222%22%0A%20%20stroke-linecap=%22round%22%0A%20%20stroke-linejoin=%22round%22%0A%3E%0A%20%20%3Cpath%20d=%22M18%205a2%202%200%200%201%202%202v8.526a2%202%200%200%200%20.212.897l1.068%202.127a1%201%200%200%201-.9%201.45H3.62a1%201%200%200%201-.9-1.45l1.068-2.127A2%202%200%200%200%204%2015.526V7a2%202%200%200%201%202-2z%22%20/%3E%0A%20%20%3Cpath%20d=%22M20.054%2015.987H3.946%22%20/%3E%0A%3C/svg%3E"); --icon-size: 18px; }
.icon-settings { --icon-url: url("data:image/svg+xml,%3Csvg%0A%20%20xmlns=%22http://www.w3.org/2000/svg%22%0A%20%20width=%2224%22%0A%20%20height=%2224%22%0A%20%20viewBox=%220%200%2024%2024%22%0A%20%20fill=%22none%22%0A%20%20stroke=%22currentColor%22%0A%20%20stroke-width=%222%22%0A%20%20stroke-linecap=%22round%22%0A%20%20stroke-linejoin=%22round%22%0A%3E%0A%20%20%3Cpath%20d=%22M9.671%204.136a2.34%202.34%200%200%201%204.659%200%202.34%202.34%200%200%200%203.319%201.915%202.34%202.34%200%200%201%202.33%204.033%202.34%202.34%200%200%200%200%203.831%202.34%202.34%200%200%201-2.33%204.033%202.34%202.34%200%200%200-3.319%201.915%202.34%202.34%200%200%201-4.659%200%202.34%202.34%200%200%200-3.32-1.915%202.34%202.34%200%200%201-2.33-4.033%202.34%202.34%200%200%200%200-3.831A2.34%202.34%200%200%201%206.35%206.051a2.34%202.34%200%200%200%203.319-1.915%22%20/%3E%0A%20%20%3Ccircle%20cx=%2212%22%20cy=%2212%22%20r=%223%22%20/%3E%0A%3C/svg%3E"); --icon-size: 18px; }
.icon-trash { --icon-url: url("data:image/svg+xml,%3Csvg%0A%20%20xmlns=%22http://www.w3.org/2000/svg%22%0A%20%20width=%2224%22%0A%20%20height=%2224%22%0A%20%20viewBox=%220%200%2024%2024%22%0A%20%20fill=%22none%22%0A%20%20stroke=%22currentColor%22%0A%20%20stroke-width=%222%22%0A%20%20stroke-linecap=%22round%22%0A%20%20stroke-linejoin=%22round%22%0A%3E%0A%20%20%3Cpath%20d=%22M19%206v14a2%202%200%200%201-2%202H7a2%202%200%200%201-2-2V6%22%20/%3E%0A%20%20%3Cpath%20d=%22M3%206h18%22%20/%3E%0A%20%20%3Cpath%20d=%22M8%206V4a2%202%200%200%201%202-2h4a2%202%200%200%201%202%202v2%22%20/%3E%0A%3C/svg%3E"); --icon-size: 18px; }
.icon-copy { --icon-url: url("data:image/svg+xml,%3Csvg%0A%20%20xmlns=%22http://www.w3.org/2000/svg%22%0A%20%20width=%2224%22%0A%20%20height=%2224%22%0A%20%20viewBox=%220%200%2024%2024%22%0A%20%20fill=%22none%22%0A%20%20stroke=%22currentColor%22%0A%20%20stroke-width=%222%22%0A%20%20stroke-linecap=%22round%22%0A%20%20stroke-linejoin=%22round%22%0A%3E%0A%20%20%3Crect%20width=%228%22%20height=%224%22%20x=%228%22%20y=%222%22%20rx=%221%22%20ry=%221%22%20/%3E%0A%20%20%3Cpath%20d=%22M16%204h2a2%202%200%200%201%202%202v14a2%202%200%200%201-2%202H6a2%202%200%200%201-2-2V6a2%202%200%200%201%202-2h2%22%20/%3E%0A%3C/svg%3E"); --icon-size: 18px; }
.icon-sun { --icon-url: url("data:image/svg+xml,%3Csvg%0A%20%20xmlns=%22http://www.w3.org/2000/svg%22%0A%20%20width=%2224%22%0A%20%20height=%2224%22%0A%20%20viewBox=%220%200%2024%2024%22%0A%20%20fill=%22none%22%0A%20%20stroke=%22currentColor%22%0A%20%20stroke-width=%222%22%0A%20%20stroke-linecap=%22round%22%0A%20%20stroke-linejoin=%22round%22%0A%3E%0A%20%20%3Cpath%20d=%22M11.017%202.814a1%201%200%200%201%201.966%200l1.051%205.558a2%202%200%200%200%201.594%201.594l5.558%201.051a1%201%200%200%201%200%201.966l-5.558%201.051a2%202%200%200%200-1.594%201.594l-1.051%205.558a1%201%200%200%201-1.966%200l-1.051-5.558a2%202%200%200%200-1.594-1.594l-5.558-1.051a1%201%200%200%201%200-1.966l5.558-1.051a2%202%200%200%200%201.594-1.594z%22%20/%3E%0A%20%20%3Cpath%20d=%22M20%202v4%22%20/%3E%0A%20%20%3Cpath%20d=%22M22%204h-4%22%20/%3E%0A%20%20%3Ccircle%20cx=%224%22%20cy=%2220%22%20r=%222%22%20/%3E%0A%3C/svg%3E"); --icon-size: 22px; }
.app { position: relative; height: 100vh; background: var(--bg); }
.main { min-width: 0; height: 100vh; margin-left: var(--expanded-width); display: flex; align-items: flex-start; justify-content: center; padding: 28px 32px; }
.main-title { margin: 0; font-size: 34px; line-height: 1.1; font-weight: 620; letter-spacing: -0.04em; }
.main-title span { color: var(--muted); font-weight: 430; }
/* 工作区:窄、扁平、无大卡片嵌套。展开的对话栏会完整盖住它。 */
.workspace-panel {
position: fixed;
left: var(--collapsed-width);
top: 0;
z-index: 20;
width: var(--workspace-width);
height: 100vh;
background: var(--panel-bg);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
padding: 18px 10px 14px;
overflow: hidden;
transition: background 160ms ease, border-color 160ms ease;
}
.workspace-title {
height: 40px;
margin: 0;
display: flex;
align-items: center;
padding: 0 0 0 16px;
font-size: 18px;
font-weight: 660;
letter-spacing: -0.035em;
white-space: nowrap;
}
.workspace-controls {
display: grid;
grid-template-columns: minmax(0, 1fr) 40px;
gap: 4px;
margin: 4px 0 10px;
}
.workspace-switch,
.workspace-manage {
height: 40px;
border: 0;
border-radius: 12px;
background: transparent;
color: var(--text);
cursor: pointer;
transition: background 140ms ease, color 140ms ease;
}
.workspace-switch:hover,
.workspace-manage:hover { background: var(--hover); }
.workspace-switch {
min-width: 0;
display: grid;
grid-template-columns: 32px minmax(0, 1fr) 18px;
align-items: center;
padding: 0 10px 0 8px;
text-align: left;
font-size: 14px;
letter-spacing: -0.012em;
}
.workspace-switch .icon { justify-self: center; color: var(--text); }
.workspace-switch-label { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.chevron { width: 8px; height: 8px; border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor; transform: rotate(45deg) translateY(-2px); opacity: .55; justify-self: center; }
.workspace-manage { display: grid; place-items: center; color: var(--text); }
.workspace-body { min-height: 0; flex: 1 1 auto; overflow: hidden; display: flex; flex-direction: column; }
.workspace-view { display: none; min-height: 0; flex: 1 1 auto; overflow-y: auto; scrollbar-width: thin; padding-top: 2px; }
.workspace-view.active { display: block; }
.content-row {
width: 100%;
height: 36px;
border-radius: 10px;
display: grid;
align-items: center;
color: var(--text);
font-size: 13px;
letter-spacing: -0.01em;
overflow: hidden;
}
.content-row + .content-row { margin-top: 1px; }
.content-main { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.content-meta { color: var(--muted); font-size: 12px; white-space: nowrap; }
/* 文件树是真实可操作树:保留文件/文件夹图标和 hover其他内容区不做按钮感。 */
.file-row {
grid-template-columns: 20px 20px minmax(0, 1fr) auto;
gap: 4px;
padding: 0 8px 0 2px;
cursor: pointer;
transition: background 140ms ease;
}
.file-row:hover { background: var(--hover); }
.file-row .icon { color: var(--muted); justify-self: center; }
.file-arrow { width: 16px; text-align: center; color: var(--muted); font-size: 11px; }
.file-row.depth-1 { padding-left: 16px; }
.file-row.depth-2 { padding-left: 30px; }
.plain-row {
grid-template-columns: minmax(0, 1fr) auto;
gap: 8px;
padding: 0 10px;
cursor: default;
}
.option-row {
height: 44px;
grid-template-columns: 20px minmax(0, 1fr) auto;
gap: 8px;
padding: 0 10px 0 8px;
cursor: pointer;
transition: background 140ms ease, color 140ms ease;
}
.option-row:hover { background: var(--hover); }
.option-row.selected { background: var(--hover); }
.option-marker {
width: 13px;
height: 13px;
border-radius: 999px;
border: 1.5px solid var(--muted);
justify-self: center;
position: relative;
opacity: .85;
}
.option-row.selected .option-marker {
border-color: var(--text);
opacity: 1;
}
.option-row.selected .option-marker::after {
content: '';
position: absolute;
inset: 3px;
border-radius: inherit;
background: var(--text);
}
.host-copy { min-width: 0; display: grid; grid-template-rows: 18px 16px; }
.host-label { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 13px; font-weight: 560; }
.host-path { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--muted); font-size: 12px; }
.host-badges { display: inline-flex; gap: 4px; align-items: center; }
.host-badge { color: var(--muted); font-size: 11px; }
.todo-instruction {
margin-top: 8px;
padding: 0 10px;
color: var(--muted);
font-size: 12px;
line-height: 1.35;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.empty-row {
height: 36px;
display: flex;
align-items: center;
padding: 0 10px;
color: var(--muted);
font-size: 13px;
}
/* 侧边栏:同一张完整侧边栏被裁切暴露。 */
.sidebar-frame {
position: fixed;
left: 0;
top: 0;
z-index: 50;
width: var(--collapsed-width);
height: 100vh;
overflow: hidden;
background: var(--panel-bg);
border-right: 1px solid var(--border);
transition: width var(--anim), background 160ms ease, border-color 160ms ease;
will-change: width;
}
.sidebar-frame.open { width: var(--expanded-width); }
.sidebar-content { width: var(--expanded-width); height: 100%; background: var(--panel-bg); display: flex; flex-direction: column; padding: 18px 8px 14px 6px; transition: background 160ms ease; }
.sidebar-top { display: flex; flex-direction: column; gap: 2px; flex: 0 0 auto; }
.sidebar-bottom { margin-top: 8px; flex: 0 0 auto; }
.side-row,
.search-shell,
.conversation-row { width: 100%; min-height: var(--row-height); border: 0; border-radius: var(--row-radius); background: transparent; color: var(--text); display: grid; grid-template-columns: var(--icon-cell) minmax(0, 1fr); align-items: center; padding: 0; text-align: left; }
.side-row,
.conversation-row { cursor: pointer; transition: background 140ms ease, color 140ms ease; }
.side-row:hover,
.conversation-row:hover { background: transparent; }
.side-icon { width: var(--icon-cell); height: var(--row-height); border-radius: var(--row-radius); display: grid; place-items: center; color: currentColor; flex: 0 0 auto; transition: background 140ms ease; }
.sidebar-frame:not(.open) .side-row:hover .side-icon,
.sidebar-frame:not(.open) .theme-toggle:hover .side-icon { background: var(--hover); }
.sidebar-frame.open .side-row:hover,
.sidebar-frame.open .side-row.active,
.sidebar-frame.open .conversation-row:hover,
.sidebar-frame.open .conversation-row.menu-open { background: var(--hover); }
.side-label,
.search-area,
.conversation-list,
.conversation-title,
.conversation-more { opacity: 0; transition: opacity 220ms ease; transition-delay: 0ms; }
.sidebar-frame.open .side-label,
.sidebar-frame.open .search-area,
.sidebar-frame.open .conversation-list,
.sidebar-frame.open .conversation-title,
.sidebar-frame.open .conversation-more { opacity: 1; transition-delay: 80ms; }
.side-label { min-width: 0; padding-left: 8px; padding-right: 12px; font-size: 15px; line-height: 1; letter-spacing: -0.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.search-area { flex: 0 0 auto; margin-top: 10px; }
.search-shell { height: var(--row-height); overflow: hidden; transition: background 140ms ease; }
.search-shell:focus-within,
.search-shell:hover { background: var(--hover); }
.search-input { min-width: 0; width: calc(var(--expanded-width) - var(--icon-cell) - 22px); height: var(--row-height); border: 0; outline: 0; padding: 0 12px 0 8px; background: transparent; color: var(--text); font-size: 14px; letter-spacing: -0.01em; }
.search-input::placeholder { color: var(--muted); }
.conversation-list { min-height: 0; flex: 1 1 auto; overflow-y: auto; padding-top: 12px; padding-right: 0; scrollbar-width: thin; }
.conversation-row { position: relative; grid-template-columns: minmax(0, 1fr) 40px; min-height: var(--row-height); margin: 0; }
.conversation-title { min-width: 0; padding-left: 12px; padding-right: 4px; color: var(--text); font-size: 14px; line-height: 1.25; letter-spacing: -0.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.conversation-more { width: 30px; height: 30px; border: 0; border-radius: 9px; background: transparent; color: var(--muted); display: grid; place-items: center; cursor: pointer; justify-self: start; visibility: hidden; transition: color 140ms ease, opacity 160ms ease; }
.conversation-row:hover .conversation-more,
.conversation-row.menu-open .conversation-more { visibility: visible; }
.conversation-more:hover,
.conversation-row.menu-open .conversation-more { background: transparent; color: var(--text); }
.more-dots {
position: relative;
display: inline-block;
width: 22px;
height: 22px;
}
.more-dots::before {
content: '';
position: absolute;
left: 4px;
top: 10px;
width: 3.5px;
height: 3.5px;
border-radius: 999px;
background: currentColor;
box-shadow: 5.5px 0 0 currentColor, 11px 0 0 currentColor;
}
.floating-menu {
position: fixed;
z-index: 80;
width: max-content;
min-width: 92px;
padding: 6px;
border: 1px solid var(--border);
border-radius: 14px;
background: var(--menu-bg);
box-shadow: var(--menu-shadow);
opacity: 0;
transform: translateY(-4px) scale(0.98);
pointer-events: none;
transition: opacity 120ms ease, transform 150ms cubic-bezier(0.22, 1, 0.36, 1), background 160ms ease, border-color 160ms ease;
}
.floating-menu.open { opacity: 1; transform: translateY(0) scale(1); pointer-events: auto; }
.menu-button { width: max-content; min-width: 80px; height: 36px; border: 0; border-radius: 10px; background: transparent; color: var(--text); display: grid; grid-template-columns: 24px auto; gap: 4px; align-items: center; padding: 0 10px 0 8px; text-align: left; cursor: pointer; font-size: 14px; letter-spacing: -0.01em; transition: background 140ms ease; }
.menu-button:hover { background: var(--hover); }
.menu-button.danger { color: #d6483b; }
:root[data-theme="dark"] .menu-button.danger { color: #ff7b72; }
.demo-hint { position: fixed; left: calc(var(--expanded-width) + 20px); bottom: 18px; color: var(--muted); font-size: 13px; pointer-events: none; }
</style>
</head>
<body>
<div class="app">
<aside class="workspace-panel" aria-label="工作区">
<h2 class="workspace-title" id="workspaceTitle">工作区</h2>
<div class="workspace-controls">
<button class="workspace-switch" id="workspaceSwitch" type="button" aria-haspopup="menu" aria-expanded="false">
<span class="icon icon-folder-open" id="workspaceSwitchIcon" aria-hidden="true"></span>
<span class="workspace-switch-label" id="workspaceSwitchLabel">工作区文件</span>
<span class="chevron" aria-hidden="true"></span>
</button>
<button class="workspace-manage" type="button" aria-label="管理工作区">
<span class="icon icon-folder" aria-hidden="true"></span>
</button>
</div>
<div class="workspace-body">
<div class="workspace-view active" data-view="files">
<div class="content-row file-row"><span class="file-arrow"></span><span class="icon icon-folder-open"></span><span class="content-main">static</span><span class="content-meta"></span></div>
<div class="content-row file-row depth-1"><span class="file-arrow"></span><span class="icon icon-file"></span><span class="content-main">App.vue</span><span class="content-meta"></span></div>
<div class="content-row file-row depth-1"><span class="file-arrow"></span><span class="icon icon-folder-open"></span><span class="content-main">components</span><span class="content-meta"></span></div>
<div class="content-row file-row depth-2"><span class="file-arrow"></span><span class="icon icon-file"></span><span class="content-main">ConversationSidebar.vue</span><span class="content-meta"></span></div>
<div class="content-row file-row depth-2"><span class="file-arrow"></span><span class="icon icon-file"></span><span class="content-main">LeftPanel.vue</span><span class="content-meta"></span></div>
<div class="content-row file-row"><span class="file-arrow"></span><span class="icon icon-folder"></span><span class="content-main">server</span><span class="content-meta"></span></div>
<div class="content-row file-row"><span class="file-arrow"></span><span class="icon icon-file"></span><span class="content-main">main.py</span><span class="content-meta"></span></div>
</div>
<div class="workspace-view" data-view="host">
<div class="content-row option-row selected"><span class="option-marker" aria-hidden="true"></span><span class="host-copy"><span class="host-label">agents</span><span class="host-path">/Users/jojo/Desktop/agents/正在修复中/agents</span></span><span class="host-badges"><span class="host-badge">默认</span><span class="host-badge">当前</span></span></div>
<div class="content-row option-row"><span class="option-marker" aria-hidden="true"></span><span class="host-copy"><span class="host-label">前端工作区</span><span class="host-path">/Users/jojo/Desktop/frontend</span></span><span class="host-badges"></span></div>
<div class="content-row option-row"><span class="option-marker" aria-hidden="true"></span><span class="host-copy"><span class="host-label">未配置路径</span><span class="host-path">(未配置路径)</span></span><span class="host-badges"></span></div>
</div>
<div class="workspace-view" data-view="agents">
<div class="content-row plain-row"><span class="content-main">#1 正在整理侧边栏与工作区样式</span><span class="content-meta">running</span></div>
<div class="content-row plain-row"><span class="content-main">#2 等待下一轮验证</span><span class="content-meta">idle</span></div>
<div class="content-row plain-row"><span class="content-main">当前read_file</span><span class="content-meta"></span></div>
</div>
<div class="workspace-view" data-view="commands">
<div class="content-row plain-row"><span class="content-main">npm --prefix cli run typecheck</span><span class="content-meta">completed</span></div>
<div class="content-row plain-row"><span class="content-main">python -m unittest test.test_server_refactor_smoke</span><span class="content-meta">running</span></div>
<div class="content-row plain-row"><span class="content-main">状态码:--</span><span class="content-meta"></span></div>
</div>
<div class="workspace-view" data-view="todos">
<div class="content-row plain-row"><span class="content-main">task1确定侧边栏动画模型</span><span class="content-meta">完成</span></div>
<div class="content-row plain-row"><span class="content-main">task2工作区视觉改造</span><span class="content-meta">未完成</span></div>
<div class="content-row plain-row"><span class="content-main">task3代码块精致化</span><span class="content-meta">未完成</span></div>
<div class="todo-instruction">指令:按真实数据结构设计显示区域,不添加额外说明卡片。</div>
</div>
</div>
</aside>
<aside class="sidebar-frame" id="sidebar" aria-label="对话侧边栏">
<div class="sidebar-content">
<div class="sidebar-top">
<button class="side-row" id="toggleSidebar" type="button" aria-label="展开对话记录" aria-expanded="false">
<span class="side-icon"><span class="icon icon-chat" aria-hidden="true"></span></span>
<span class="side-label">对话记录</span>
</button>
<button class="side-row" type="button" aria-label="新建对话"><span class="side-icon"><span class="icon icon-pencil"></span></span><span class="side-label">新建对话</span></button>
<button class="side-row" type="button" aria-label="工作区"><span class="side-icon"><span class="icon icon-layers"></span></span><span class="side-label">工作区</span></button>
<button class="side-row" type="button" aria-label="虚拟显示器"><span class="side-icon"><span class="icon icon-monitor"></span></span><span class="side-label">虚拟显示器</span></button>
</div>
<div class="search-area" aria-label="搜索对话"><label class="search-shell"><span class="side-icon"><span class="icon icon-search"></span></span><input class="search-input" type="search" placeholder="搜索对话" /></label></div>
<div class="conversation-list" aria-label="对话记录">
<div class="conversation-row"><span class="conversation-title">随意的 py 代码块</span><button class="conversation-more" type="button" aria-label="更多操作"><span class="more-dots" aria-hidden="true"></span></button></div>
<div class="conversation-row"><span class="conversation-title">前端现代化设计讨论</span><button class="conversation-more" type="button" aria-label="更多操作"><span class="more-dots" aria-hidden="true"></span></button></div>
<div class="conversation-row"><span class="conversation-title">审批面板覆盖窗口方案</span><button class="conversation-more" type="button" aria-label="更多操作"><span class="more-dots" aria-hidden="true"></span></button></div>
<div class="conversation-row"><span class="conversation-title">CLI 显示层细节修复</span><button class="conversation-more" type="button" aria-label="更多操作"><span class="more-dots" aria-hidden="true"></span></button></div>
<div class="conversation-row"><span class="conversation-title">个人空间设置页重设计</span><button class="conversation-more" type="button" aria-label="更多操作"><span class="more-dots" aria-hidden="true"></span></button></div>
</div>
<div class="sidebar-bottom">
<button class="side-row theme-toggle" id="themeToggle" type="button" aria-label="切换深色模式"><span class="side-icon"><span class="icon icon-sun"></span></span><span class="side-label">浅色模式</span></button>
<button class="side-row" type="button" aria-label="个人空间"><span class="side-icon"><span class="icon icon-user"></span></span><span class="side-label">个人空间</span></button>
</div>
</div>
</aside>
<main class="main"><h1 class="main-title">AI Agent <span>Auto</span></h1></main>
<div class="floating-menu" id="conversationMenu" role="menu" aria-hidden="true">
<button class="menu-button" type="button" role="menuitem"><span class="icon icon-copy"></span><span>复制</span></button>
<button class="menu-button danger" type="button" role="menuitem"><span class="icon icon-trash"></span><span>删除</span></button>
</div>
<div class="floating-menu" id="workspaceMenu" role="menu" aria-hidden="true">
<button class="menu-button" type="button" data-view-target="files"><span class="icon icon-folder-open"></span><span>工作区文件</span></button>
<button class="menu-button" type="button" data-view-target="host"><span class="icon icon-laptop"></span><span>宿主机工作区</span></button>
<button class="menu-button" type="button" data-view-target="agents"><span class="icon icon-bot"></span><span>子智能体</span></button>
<button class="menu-button" type="button" data-view-target="commands"><span class="icon icon-terminal"></span><span>后台指令</span></button>
<button class="menu-button" type="button" data-view-target="todos"><span class="icon icon-sticky"></span><span>待办事项</span></button>
</div>
</div>
<div class="demo-hint">工作区为扁平窄面板;展开侧边栏会完整盖住工作区。</div>
<script>
const sidebar = document.getElementById('sidebar');
const toggle = document.getElementById('toggleSidebar');
const themeToggle = document.getElementById('themeToggle');
const conversationMenu = document.getElementById('conversationMenu');
const workspaceMenu = document.getElementById('workspaceMenu');
const workspaceSwitch = document.getElementById('workspaceSwitch');
const workspaceTitle = document.getElementById('workspaceTitle');
const workspaceSwitchLabel = document.getElementById('workspaceSwitchLabel');
const workspaceSwitchIcon = document.getElementById('workspaceSwitchIcon');
let conversationOwner = null;
const viewMeta = {
files: { title: '工作区', label: '工作区文件', icon: 'icon-folder-open' },
host: { title: '宿主机工作区', label: '宿主机工作区', icon: 'icon-laptop' },
agents: { title: '子智能体', label: '子智能体', icon: 'icon-bot' },
commands: { title: '后台指令', label: '后台指令', icon: 'icon-terminal' },
todos: { title: '待办事项', label: '待办事项', icon: 'icon-sticky' }
};
toggle.addEventListener('click', () => {
const isOpen = sidebar.classList.toggle('open');
toggle.setAttribute('aria-expanded', String(isOpen));
toggle.setAttribute('aria-label', isOpen ? '收起对话记录' : '展开对话记录');
if (!isOpen) closeConversationMenu();
});
themeToggle.addEventListener('click', () => {
const root = document.documentElement;
const next = root.dataset.theme === 'dark' ? 'light' : 'dark';
root.dataset.theme = next;
themeToggle.querySelector('.side-label').textContent = next === 'dark' ? '深色模式' : '浅色模式';
});
function positionMenu(menu, anchor) {
const rect = anchor.getBoundingClientRect();
menu.style.left = '0px';
menu.style.top = `${rect.bottom + 6}px`;
menu.classList.add('open');
menu.setAttribute('aria-hidden', 'false');
const width = menu.offsetWidth;
menu.style.left = `${Math.min(rect.right - width, window.innerWidth - width - 12)}px`;
}
function closeConversationMenu() {
conversationMenu.classList.remove('open');
conversationMenu.setAttribute('aria-hidden', 'true');
if (conversationOwner) conversationOwner.classList.remove('menu-open');
conversationOwner = null;
}
function closeWorkspaceMenu() {
workspaceMenu.classList.remove('open');
workspaceMenu.setAttribute('aria-hidden', 'true');
workspaceSwitch.setAttribute('aria-expanded', 'false');
}
document.querySelectorAll('.conversation-more').forEach((button) => {
button.addEventListener('click', (event) => {
event.stopPropagation();
closeWorkspaceMenu();
const row = button.closest('.conversation-row');
if (conversationOwner === row && conversationMenu.classList.contains('open')) {
closeConversationMenu();
return;
}
closeConversationMenu();
conversationOwner = row;
row.classList.add('menu-open');
positionMenu(conversationMenu, button);
});
});
workspaceSwitch.addEventListener('click', (event) => {
event.stopPropagation();
closeConversationMenu();
if (workspaceMenu.classList.contains('open')) {
closeWorkspaceMenu();
} else {
workspaceSwitch.setAttribute('aria-expanded', 'true');
positionMenu(workspaceMenu, workspaceSwitch);
}
});
workspaceMenu.querySelectorAll('[data-view-target]').forEach((button) => {
button.addEventListener('click', () => {
const view = button.dataset.viewTarget;
document.querySelectorAll('.workspace-view').forEach((node) => node.classList.toggle('active', node.dataset.view === view));
const meta = viewMeta[view];
workspaceTitle.textContent = meta.title;
workspaceSwitchLabel.textContent = meta.label;
workspaceSwitchIcon.className = `icon ${meta.icon}`;
closeWorkspaceMenu();
});
});
document.addEventListener('click', (event) => {
if (!conversationMenu.contains(event.target)) closeConversationMenu();
if (!workspaceMenu.contains(event.target) && !workspaceSwitch.contains(event.target)) closeWorkspaceMenu();
});
window.addEventListener('resize', () => {
closeConversationMenu();
closeWorkspaceMenu();
});
</script>
</body>
</html>

View File

@ -226,7 +226,10 @@
> >
默认工作区 默认工作区
</span> </span>
<span v-if="item.workspace_id === hostWorkspaceId" class="host-workspace-badge current"> <span
v-if="item.workspace_id === hostWorkspaceId"
class="host-workspace-badge current"
>
当前 当前
</span> </span>
</span> </span>
@ -237,7 +240,9 @@
</button> </button>
</div> </div>
<div v-else class="file-tree-empty">暂无工作区点击右侧 + 新建</div> <div v-else class="file-tree-empty">暂无工作区点击右侧 + 新建</div>
<div class="host-workspace-hint" v-if="hostWorkspaceSwitching">正在切换请稍候</div> <div class="host-workspace-hint" v-if="hostWorkspaceSwitching">
正在切换请稍候
</div>
</div> </div>
<div v-else class="file-tree-empty">{{ fileTreeMessage }}</div> <div v-else class="file-tree-empty">{{ fileTreeMessage }}</div>
</template> </template>

View File

@ -1,181 +1,106 @@
<template> <template>
<aside class="conversation-sidebar" :class="{ collapsed, 'monitor-mode': monitorModeActive }"> <aside class="conversation-sidebar" :class="{ collapsed, 'monitor-mode': monitorModeActive }">
<div class="conversation-header" :class="{ 'collapsed-layout': collapsed }"> <div class="conversation-sidebar-inner">
<template v-if="collapsed"> <div class="conversation-primary-actions">
<div class="collapsed-header-buttons">
<button
type="button"
class="collapsed-control-btn conversation-menu-btn"
title="展开对话记录"
@click="$emit('toggle')"
>
<span class="sr-only">展开对话记录</span>
<span class="chat-icon" data-tutorial="conversation-menu" aria-hidden="true">
<slot name="collapsed-chat-icon">
<svg viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M5 6.5c0-1.38 1.12-2.5 2.5-2.5h13c1.38 0 2.5 1.12 2.5 2.5v8.5c0 1.38-1.12 2.5-2.5 2.5h-5.6l-3.4 3.2.6-3.2H7.5c-1.38 0-2.5-1.12-2.5-2.5V6.5z"
stroke="currentColor"
stroke-width="1.7"
stroke-linejoin="round"
/>
<path
d="M9 9.5h10"
stroke="currentColor"
stroke-width="1.7"
stroke-linecap="round"
/>
<path
d="M9 13h6"
stroke="currentColor"
stroke-width="1.7"
stroke-linecap="round"
/>
</svg>
</slot>
</span>
</button>
<button
type="button"
class="collapsed-control-btn quick-plus-btn"
title="快捷新建对话"
@click="$emit('create')"
>
<span class="sr-only">新建对话</span>
<span class="pencil-icon" data-tutorial="quick-new-conversation" aria-hidden="true">
<slot name="collapsed-create-icon">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"
fill="none"
/>
<path d="m15 5 4 4" fill="none" />
</svg>
</slot>
</span>
</button>
<button
type="button"
class="collapsed-control-btn workspace-control-btn"
:class="{ active: workspaceCollapsed }"
:title="workspaceCollapsed ? '显示工作区' : '隐藏工作区'"
@click="$emit('toggle-workspace')"
>
<span class="sr-only">{{ workspaceCollapsed ? '显示工作区' : '隐藏工作区' }}</span>
<span
class="icon icon-md"
data-tutorial="workspace-toggle"
aria-hidden="true"
:style="iconStyle('layers')"
></span>
</button>
<button
type="button"
class="collapsed-control-btn monitor-mode-btn"
:class="{ active: displayMode === 'monitor', blocked: displayModeDisabled }"
@click="$emit('toggle-display-mode')"
:aria-disabled="displayModeDisabled"
:title="displayMode === 'monitor' ? '退出显示器' : '虚拟显示器'"
>
<span class="sr-only">{{
displayMode === 'monitor' ? '退出显示器' : '虚拟显示器'
}}</span>
<span
class="icon icon-md"
data-tutorial="monitor-toggle"
aria-hidden="true"
:style="iconStyle('monitor')"
></span>
</button>
</div>
</template>
<template v-else>
<button <button
class="new-conversation-btn" type="button"
:class="{ 'icon-only': monitorModeActive }" class="sidebar-nav-row conversation-menu-btn"
:title="monitorModeActive ? '新建对话' : undefined" :title="collapsed ? '展开对话记录' : '收起对话记录'"
@click="$emit('create')"
>
<template v-if="monitorModeActive">
<span
class="monitor-icon-slot icon icon-md"
:style="iconStyle('pencil')"
aria-hidden="true"
></span>
<span class="sr-only">新建对话</span>
</template>
<template v-else>
<span class="btn-icon">+</span>
<span class="btn-text">新建对话</span>
</template>
</button>
<button
v-if="resolvedShowCollapseButton"
class="toggle-sidebar-btn"
data-tutorial="conversation-collapse"
:class="{ 'icon-only': monitorModeActive }"
:title="monitorModeActive ? (collapsed ? '展开对话记录' : '折叠对话记录') : undefined"
@click="$emit('toggle')" @click="$emit('toggle')"
> >
<template v-if="resolvedCollapseButtonVariant === 'toggle'"> <span
<template v-if="monitorModeActive"> class="sidebar-nav-icon chat-icon"
<span data-tutorial="conversation-menu"
class="monitor-icon-slot icon icon-md" aria-hidden="true"
:style="iconStyle(collapsed ? 'menu' : 'x')" >
aria-hidden="true" <svg viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
></span> <path
<span class="sr-only">{{ collapsed ? '展开对话记录' : '折叠对话记录' }}</span> d="M5 6.5c0-1.38 1.12-2.5 2.5-2.5h13c1.38 0 2.5 1.12 2.5 2.5v8.5c0 1.38-1.12 2.5-2.5 2.5h-5.6l-3.4 3.2.6-3.2H7.5c-1.38 0-2.5-1.12-2.5-2.5V6.5z"
</template> stroke="currentColor"
<template v-else> stroke-width="1.45"
<span stroke-linejoin="round"
v-if="collapsed" />
class="icon icon-md" <path
:style="iconStyle('menu')" d="M9 9.5h10"
aria-hidden="true" stroke="currentColor"
></span> stroke-width="1.45"
<span v-else class="toggle-arrow" aria-hidden="true"></span> stroke-linecap="round"
</template> />
</template> <path d="M9 13h6" stroke="currentColor" stroke-width="1.45" stroke-linecap="round" />
<template v-else> </svg>
<template v-if="monitorModeActive"> </span>
<span <span class="sidebar-nav-label">对话记录</span>
class="monitor-icon-slot icon icon-md"
:style="iconStyle('x')"
aria-hidden="true"
></span>
<span class="sr-only">关闭侧边栏</span>
</template>
<template v-else>
<span class="toggle-close" aria-hidden="true">×</span>
</template>
</template>
</button> </button>
</template>
</div>
<template v-if="!collapsed"> <button type="button" class="sidebar-nav-row" title="新建对话" @click="$emit('create')">
<span
class="sidebar-nav-icon pencil-icon"
data-tutorial="quick-new-conversation"
aria-hidden="true"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.6"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"
/>
<path d="m15 5 4 4" />
</svg>
</span>
<span class="sidebar-nav-label">新建对话</span>
</button>
<button
type="button"
class="sidebar-nav-row workspace-control-btn"
:class="{ active: workspaceCollapsed }"
:title="workspaceCollapsed ? '显示工作区' : '隐藏工作区'"
@click="$emit('toggle-workspace')"
>
<span class="sidebar-nav-icon" data-tutorial="workspace-toggle" aria-hidden="true">
<span class="icon icon-md" :style="iconStyle('layers')"></span>
</span>
<span class="sidebar-nav-label">工作区</span>
</button>
<button
type="button"
class="sidebar-nav-row monitor-mode-btn"
:class="{ active: displayMode === 'monitor', blocked: displayModeDisabled }"
:aria-disabled="displayModeDisabled"
:title="displayMode === 'monitor' ? '退出显示器' : '虚拟显示器'"
@click="$emit('toggle-display-mode')"
>
<span class="sidebar-nav-icon" data-tutorial="monitor-toggle" aria-hidden="true">
<span class="icon icon-md" :style="iconStyle('monitor')"></span>
</span>
<span class="sidebar-nav-label">虚拟显示器</span>
</button>
</div>
<div class="conversation-search"> <div class="conversation-search">
<div class="search-input-wrap"> <label class="search-input-wrap">
<span class="sidebar-nav-icon search-inline-icon" aria-hidden="true">
<span class="icon icon-sm" :style="iconStyle('search')"></span>
</span>
<input <input
class="search-input" class="search-input"
:value="searchQuery" :value="searchQuery"
placeholder="搜索对话..." placeholder="搜索对话"
@input="$emit('search', ($event.target as HTMLInputElement).value)" @input="$emit('search', ($event.target as HTMLInputElement).value)"
@keydown.enter.prevent=" @keydown.enter.prevent="
$emit('search-submit', ($event.target as HTMLInputElement).value) $emit('search-submit', ($event.target as HTMLInputElement).value)
" "
/> />
</div> </label>
</div> </div>
<div class="conversation-list"> <div class="conversation-list">
@ -194,33 +119,53 @@
:key="conv.id" :key="conv.id"
class="conversation-item" class="conversation-item"
:class="{ active: conv.id === currentConversationId }" :class="{ active: conv.id === currentConversationId }"
@click="$emit('select', conv.id)" @click="
openActionMenuId = null;
$emit('select', conv.id);
"
> >
<div class="conversation-title">{{ conv.title }}</div> <div class="conversation-title">{{ conv.title }}</div>
<div class="conversation-meta"> <div
<span class="conversation-time">{{ formatTime(conv.updated_at) }}</span> class="conversation-action-wrap"
<span class="conversation-counts"> :class="{ 'menu-open': openActionMenuId === conv.id }"
{{ conv.total_messages || 0 }}条消息 @click.stop
<span v-if="(conv.total_tools || 0) > 0"> · {{ conv.total_tools }}工具</span> >
</span>
</div>
<div class="conversation-actions" @click.stop>
<button <button
type="button" type="button"
class="conversation-action-btn delete-btn" class="conversation-more-btn"
title="删除对话" title="更多操作"
@click="$emit('delete', conv.id)" aria-label="更多操作"
:aria-expanded="openActionMenuId === conv.id"
@click="toggleActionMenu(conv.id)"
> >
× <span aria-hidden="true"></span>
</button> </button>
<button <div
type="button" class="conversation-actions-menu"
class="conversation-action-btn copy-btn" :class="{ open: openActionMenuId === conv.id }"
title="复制对话"
@click="$emit('duplicate', conv.id)"
> >
<button
</button> type="button"
@click="
openActionMenuId = null;
$emit('duplicate', conv.id);
"
>
<span class="icon icon-sm" :style="iconStyle('copy')" aria-hidden="true"></span>
<span>复制</span>
</button>
<button
type="button"
class="danger"
@click="
openActionMenuId = null;
$emit('delete', conv.id);
"
>
<span class="icon icon-sm" :style="iconStyle('trash')" aria-hidden="true"></span>
<span>删除</span>
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -254,35 +199,27 @@
<div v-else-if="displayConversations.length" class="search-done">已全部搜索</div> <div v-else-if="displayConversations.length" class="search-done">已全部搜索</div>
</div> </div>
</div> </div>
</template>
<template v-else>
<div class="conversation-collapsed-spacer"></div>
</template>
<div class="conversation-personal-entry" :class="{ collapsed, active: personalPageVisible }"> <div class="conversation-personal-entry" :class="{ active: personalPageVisible }">
<button <button
type="button" type="button"
class="personal-page-btn" class="sidebar-nav-row personal-page-btn"
data-tutorial="open-personal-space" data-tutorial="open-personal-space"
:class="{ 'icon-only': collapsed }" title="个人空间"
title="个人页面" @click="$emit('personal')"
@click="$emit('personal')"
>
<span class="sr-only">个人页面</span>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
> >
<path <span class="sidebar-nav-icon" aria-hidden="true">
fill-rule="evenodd" <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
clip-rule="evenodd" <path
d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" fill-rule="evenodd"
></path> clip-rule="evenodd"
</svg> d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"
<span class="personal-label" v-if="!collapsed">个人空间</span> ></path>
</button> </svg>
</span>
<span class="sidebar-nav-label personal-label">个人空间</span>
</button>
</div>
</div> </div>
</aside> </aside>
</template> </template>
@ -290,7 +227,7 @@
<script setup lang="ts"> <script setup lang="ts">
defineOptions({ name: 'ConversationSidebar' }); defineOptions({ name: 'ConversationSidebar' });
import { computed } from 'vue'; import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useUiStore } from '@/stores/ui'; import { useUiStore } from '@/stores/ui';
import { useConversationStore } from '@/stores/conversation'; import { useConversationStore } from '@/stores/conversation';
@ -325,6 +262,7 @@ defineEmits<{
(event: 'delete', id: string): void; (event: 'delete', id: string): void;
(event: 'duplicate', id: string): void; (event: 'duplicate', id: string): void;
(event: 'toggle-workspace'): void; (event: 'toggle-workspace'): void;
(event: 'toggle-display-mode'): void;
}>(); }>();
const uiStore = useUiStore(); const uiStore = useUiStore();
@ -350,15 +288,30 @@ const {
} = storeToRefs(conversationStore); } = storeToRefs(conversationStore);
const { visible: personalPageVisible } = storeToRefs(personalizationStore); const { visible: personalPageVisible } = storeToRefs(personalizationStore);
const formatTime = (value: unknown) => (props.formatTime ? props.formatTime(value) : String(value));
const iconStyle = (key: string) => (props.iconStyle ? props.iconStyle(key) : {}); const iconStyle = (key: string) => (props.iconStyle ? props.iconStyle(key) : {});
const resolvedShowCollapseButton = computed(() => props.showCollapseButton);
const resolvedCollapseButtonVariant = computed(() => props.collapseButtonVariant);
const displayMode = computed(() => props.displayMode); const displayMode = computed(() => props.displayMode);
const monitorModeActive = computed( const monitorModeActive = computed(
() => displayMode.value === 'monitor' || uiChatDisplayMode.value === 'monitor' () => displayMode.value === 'monitor' || uiChatDisplayMode.value === 'monitor'
); );
const displayModeDisabled = computed(() => props.displayModeDisabled); const displayModeDisabled = computed(() => props.displayModeDisabled);
const openActionMenuId = ref<string | null>(null);
const toggleActionMenu = (conversationId: string) => {
openActionMenuId.value = openActionMenuId.value === conversationId ? null : conversationId;
};
const closeActionMenu = () => {
openActionMenuId.value = null;
};
onMounted(() => {
document.addEventListener('click', closeActionMenu);
});
onBeforeUnmount(() => {
document.removeEventListener('click', closeActionMenu);
});
const displayConversations = computed(() => const displayConversations = computed(() =>
searchActive.value ? searchResults.value : conversations.value searchActive.value ? searchResults.value : conversations.value
); );

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -3,37 +3,33 @@
/* ========================================= */ /* ========================================= */
@media (max-width: 1200px) { @media (max-width: 1200px) {
.conversation-sidebar { .left-sidebar,
width: 260px; .right-sidebar {
} width: 300px !important;
}
.left-sidebar,
.right-sidebar {
width: 300px !important;
}
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.conversation-sidebar { .conversation-sidebar {
position: absolute; position: absolute;
left: 0; left: 0;
top: 0; top: 0;
height: 100%; height: 100%;
z-index: 1000; z-index: 1000;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1); box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
transform: translateX(-100%); transform: translateX(-100%);
} }
.conversation-sidebar:not(.collapsed) { .conversation-sidebar:not(.collapsed) {
transform: translateX(0); transform: translateX(0);
} }
.left-sidebar, .left-sidebar,
.right-sidebar { .right-sidebar {
display: none; display: none;
} }
.resize-handle { .resize-handle {
display: none; display: none;
} }
} }

View File

@ -10,6 +10,7 @@ export const ICONS = Object.freeze({
checkbox: '/static/icons/checkbox.svg', checkbox: '/static/icons/checkbox.svg',
circleAlert: '/static/icons/circle-alert.svg', circleAlert: '/static/icons/circle-alert.svg',
clipboard: '/static/icons/clipboard.svg', clipboard: '/static/icons/clipboard.svg',
copy: '/static/icons/copy.svg',
clock: '/static/icons/clock.svg', clock: '/static/icons/clock.svg',
eye: '/static/icons/eye.svg', eye: '/static/icons/eye.svg',
file: '/static/icons/file.svg', file: '/static/icons/file.svg',