Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
584 changes: 584 additions & 0 deletions docs/specs/2026-09-20-sidebar-spacing-design.md

Large diffs are not rendered by default.

126 changes: 126 additions & 0 deletions web/public/sidebar-spacing-preview.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ACECode Sidebar Spacing Preview</title>
<style>
:root {
color-scheme: dark;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--bg: #171717;
--panel: #202020;
--panel-hover: #2b2b2b;
--active: #343434;
--border: #373737;
--text: #e8e8e8;
--muted: #999;
--faint: #707070;
--accent: #d7a85c;
}
* { box-sizing: border-box; }
body { margin: 0; min-height: 100vh; background: #101010; color: var(--text); }
.preview { min-height: 100vh; display: flex; flex-direction: column; }
.toolbar { min-height: 58px; display: flex; align-items: center; gap: 12px; padding: 10px 18px; border-bottom: 1px solid var(--border); background: #191919; }
.toolbar strong { font-size: 14px; letter-spacing: .02em; }
.toolbar span { color: var(--muted); font-size: 12px; }
.toolbar button { margin-left: auto; border: 1px solid #555; border-radius: 6px; padding: 7px 11px; color: var(--text); background: #292929; cursor: pointer; }
.toolbar button:hover { background: #3a3a3a; }
.workspace { flex: 1; display: flex; min-height: 0; }
.sidebar { width: 330px; min-height: calc(100vh - 58px); overflow: auto; background: var(--panel); border-right: 1px solid var(--border); padding-bottom: 18px; }
.main { flex: 1; padding: 42px; color: var(--muted); }
.brand { display: flex; align-items: center; gap: 11px; padding: 12px 18px; height: 62px; border-bottom: 1px solid #2b2b2b; }
.brand-mark { width: 24px; height: 24px; border: 2px solid var(--accent); border-radius: 7px; transform: rotate(45deg); }
.brand-name { color: var(--text); font-weight: 650; font-size: 15px; }
.brand-version { display: block; margin-top: 2px; color: var(--faint); font-size: 10px; }
.fixed-nav { display: flex; flex-direction: column; gap: 2px; padding: 10px 0 2px; }
.nav-row, .tree-row { height: 32px; display: grid; align-items: center; }
.nav-row { grid-template-columns: 24px minmax(0, 1fr); gap: 7px; padding: 0 12px 0 19px; color: var(--text); font-size: 13px; }
.nav-row:hover, .tree-row:hover { background: var(--panel-hover); }
.icon { color: var(--muted); text-align: center; font-size: 13px; }
.section-header { height: 24px; display: flex; align-items: center; gap: 2px; padding: 0 12px 0 19px; margin-top: 8px; color: var(--muted); font-size: 12px; font-weight: 600; }
.section-header .chevron { width: 24px; margin-right: 0; color: var(--faint); }
.section-list { display: flex; flex-direction: column; gap: 2px; margin-top: 2px; }
.tree-row { grid-template-columns: 24px minmax(0, 1fr) 76px; column-gap: 7px; margin: 0 6px; padding: 0 8px 0 13px; border-radius: 6px; font-size: 13px; }
.tree-row .title { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.tree-row .meta { min-width: 0; display: flex; justify-content: flex-end; gap: 7px; color: var(--muted); font-size: 11px; }
.tree-row.active { background: var(--active); }
.subtree { display: flex; flex-direction: column; gap: 2px; margin-top: 2px; margin-bottom: 8px; }
.workspace-row .icon { color: #a9c58b; }
.session-row .icon { color: #8c8c8c; }
.empty { color: var(--faint); font-style: italic; }
.show-more { color: #b9b9b9; cursor: pointer; }
.show-more:hover { color: white; }
.hint { max-width: 630px; line-height: 1.7; }
.hint h1 { margin: 0 0 12px; color: var(--text); font-size: 22px; }
.hint code { padding: 2px 5px; border-radius: 4px; background: #282828; color: #e0bd7e; }
.measure { position: fixed; right: 22px; bottom: 18px; padding: 8px 11px; border: 1px solid var(--border); border-radius: 6px; background: #222; color: var(--muted); font-size: 11px; }
@media (max-width: 700px) { .main { display: none; } .sidebar { width: 100%; } .toolbar span { display: none; } }
</style>
</head>
<body>
<div class="preview">
<header class="toolbar">
<strong>ACECode 侧边栏间距预览</strong>
<span>独立预览,不需要访问令牌,不连接 daemon</span>
<button id="toggle">折叠工作区</button>
</header>
<div class="workspace">
<aside class="sidebar">
<div class="brand"><div class="brand-mark"></div><div><div class="brand-name">ACECode</div><span class="brand-version">Frontend preview</span></div></div>
<div class="fixed-nav">
<div class="nav-row"><span class="icon">+</span><span>新建任务</span></div>
<div class="nav-row"><span class="icon">◷</span><span>定时任务</span></div>
<div class="nav-row"><span class="icon">◇</span><span>扩展</span></div>
</div>

<div class="section-header"><span class="chevron">⌄</span><span>任务 <small>(3)</small></span></div>
<div class="section-list">
<div class="tree-row active"><span class="icon">●</span><span class="title">调整侧边栏间距</span><span class="meta">刚刚</span></div>
<div class="tree-row"><span class="icon">○</span><span class="title">分析 AceCode 与 Trae 页面间距差异</span><span class="meta">2 分钟</span></div>
<div class="tree-row"><span class="icon">○</span><span class="title">验证窄窗口下的布局效果</span><span class="meta">昨天</span></div>
</div>

<div class="section-header"><span class="chevron">⌄</span><span>工作区 <small>(3)</small></span></div>
<div class="section-list" id="workspace-list">
<div class="workspace-group">
<div class="tree-row workspace-row"><span class="icon">▾</span><span class="title">acecode</span><span class="meta">⋮ +</span></div>
<div class="subtree">
<div class="tree-row session-row"><span class="icon">○</span><span class="title">侧边栏视觉节奏</span><span class="meta">10:42</span></div>
<div class="tree-row session-row"><span class="icon">○</span><span class="title">开发环境启动</span><span class="meta">09:18</span></div>
<div class="tree-row session-row"><span class="icon">○</span><span class="title">展开显示</span><span class="meta"></span></div>
</div>
</div>
<div class="workspace-group"><div class="tree-row workspace-row"><span class="icon">▸</span><span class="title">tui-studio-i18n</span><span class="meta">⋮</span></div></div>
<div class="workspace-group"><div class="tree-row workspace-row"><span class="icon">▸</span><span class="title">万能服务</span><span class="meta">⋮</span></div></div>
</div>

<div class="section-header"><span class="chevron">⌄</span><span>会话 <small>(2)</small></span></div>
<div class="section-list">
<div class="tree-row"><span class="icon">○</span><span class="title">无工作区会话示例</span><span class="meta">11:06</span></div>
<div class="tree-row"><span class="icon">○</span><span class="title">权限请求状态示例</span><span class="meta">等待</span></div>
</div>
</aside>
<main class="main">
<div class="hint">
<h1>间距验证预览</h1>
<p>左侧使用本次方案的纵向节奏:普通行 <code>32px</code>、组内间距 <code>2px</code>、分组标题 <code>24px</code>、分组间距 <code>8px</code>。</p>
<p>横向树形行保持 <code>24px / minmax(0,1fr) / 76px</code>,列间距 <code>7px</code>。点击顶部按钮可以切换工作区内容,观察展开和折叠状态。</p>
</div>
</main>
</div>
<div class="measure">PROTOTYPE · no token required</div>
</div>
<script>
const button = document.querySelector('#toggle');
const list = document.querySelector('#workspace-list');
let expanded = true;
button.addEventListener('click', () => {
expanded = !expanded;
list.querySelector('.subtree').style.display = expanded ? 'flex' : 'none';
list.querySelector('.workspace-row .icon').textContent = expanded ? '▾' : '▸';
button.textContent = expanded ? '折叠工作区' : '展开工作区';
});
</script>
</body>
</html>
37 changes: 15 additions & 22 deletions web/scripts/check-session-loading-motion.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

import argparse
import json
import math
import subprocess
from html.parser import HTMLParser
from pathlib import Path
Expand Down Expand Up @@ -66,13 +65,13 @@ def production_markup():

SNAPSHOT = '''() => {
const host = document.querySelector('.ace-session-loading');
const orbit = host.querySelector('.ace-session-loading-orbit');
const dots = [...host.querySelectorAll('.ace-session-loading-dot')];
const center = node => { const r = node.getBoundingClientRect(); return [r.x+r.width/2, r.y+r.height/2]; };
return { centers:dots.map(center), hostCenter:center(host),
diameter:dots.map(node => parseFloat(getComputedStyle(node).width)),
slot:[host.offsetWidth, host.offsetHeight], opacity:parseFloat(getComputedStyle(orbit).opacity),
transforms:[orbit, ...dots].map(node => getComputedStyle(node).transform),
slot:[host.offsetWidth, host.offsetHeight],
opacity:dots.map(node => parseFloat(getComputedStyle(node).opacity)),
transforms:dots.map(node => getComputedStyle(node).transform),
label:host.getAttribute('aria-label') };
}'''

Expand Down Expand Up @@ -119,23 +118,18 @@ def mount(theme='light', motion='no-preference', width=390):
}''', SNAPSHOT)
for frame in frames:
assert frame['slot'] == [16, 16], frame
assert len(frame['centers']) == 4 and frame['label'] == 'Running'
assert all(abs(d - 4.3) < 0.01 for d in frame['diameter'])
assert all(math.dist(frame['centers'][a], frame['centers'][b]) > 4.3
for a in range(4) for b in range(a + 1, 4))
centroid = [sum(c[axis] for c in frame['centers']) / 4 for axis in [0, 1]]
assert math.dist(centroid, frame['hostCenter']) < 0.02
assert len(frame['centers']) == 12 and frame['label'] == 'Running'
assert all(abs(d - 2) < 0.01 for d in frame['diameter'])
assert len({tuple(c) for c in frame['centers']}) == 12
if motion == 'reduce':
assert all(frame['transforms'] == frames[0]['transforms'] for frame in frames)
opacity = [frame['opacity'] for frame in frames]
assert min(opacity) >= 0.599 and max(opacity) <= 1
assert max(opacity) - min(opacity) > 0.39, 'Reduced motion must retain gentle running feedback'
assert all(opacity == 1 for frame in frames for opacity in frame['opacity'])
else:
assert all(math.dist(a['centers'][0], b['centers'][0]) > 0.02
for a, b in zip(frames, frames[1:])), 'Running motion must not stall'
assert any(a['opacity'] != b['opacity']
for a, b in zip(frames, frames[1:])), 'Dot-matrix animation must not stall'
results.append({'browser': browser.version, 'theme': theme, 'width': width, 'motion': motion, 'passed': True})

# Live preference changes must work without manually controlling animations.
# Live preference changes must stop and resume the dot pulse.
mount()
for motion in ['no-preference', 'reduce', 'no-preference']:
page.emulate_media(reduced_motion=motion)
Expand All @@ -144,16 +138,15 @@ def mount(theme='light', motion='no-preference', width=390):
last = page.evaluate(SNAPSHOT)
if motion == 'reduce':
assert first['transforms'] == last['transforms']
assert abs(first['opacity'] - last['opacity']) > 0.01
assert first['opacity'] == last['opacity'] == [1] * 12
else:
assert first['transforms'] != last['transforms']
assert first['opacity'] != last['opacity']

# Defined resting positions keep all four dots separate before animation starts.
# The resting dot matrix remains an abstract A with twelve distinct points.
mount()
page.add_style_tag(content='.ace-session-loading * { animation: none !important; }')
page.add_style_tag(content='.ace-session-loading * { animation: none !important; opacity: 1 !important; }')
resting = page.evaluate(SNAPSHOT)
assert all(math.dist(c, resting['hostCenter']) > 3.4 for c in resting['centers'])
assert len({tuple(c) for c in resting['centers']}) == 4
assert len({tuple(c) for c in resting['centers']}) == 12
if args.screenshot_dir:
args.screenshot_dir.mkdir(parents=True, exist_ok=True)
page.locator('.ace-session-loading').screenshot(
Expand Down
7 changes: 2 additions & 5 deletions web/src/components/DesktopContextMenu.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,9 @@ const ACTION_LABELS = {
[DESKTOP_CONTEXT_ACTIONS.COPY_SESSION_ID]: '复制ID',
[DESKTOP_CONTEXT_ACTIONS.EXPORT_SESSION]: '导出',
[DESKTOP_CONTEXT_ACTIONS.ARCHIVE_SESSION]: '归档',
[DESKTOP_CONTEXT_ACTIONS.ACTIVATE_WORKSPACE]: '切换到项目',
[DESKTOP_CONTEXT_ACTIONS.EXPAND_WORKSPACE]: '展开项目',
[DESKTOP_CONTEXT_ACTIONS.COLLAPSE_WORKSPACE]: '折叠项目',
[DESKTOP_CONTEXT_ACTIONS.NEW_WORKSPACE_SESSION]: '新建会话',
[DESKTOP_CONTEXT_ACTIONS.NEW_WORKSPACE_SESSION]: '新建任务',
[DESKTOP_CONTEXT_ACTIONS.IMPORT_OPENCODE_SESSIONS]: '从opencode导入会话',
[DESKTOP_CONTEXT_ACTIONS.RENAME_WORKSPACE]: '重命名项目',
[DESKTOP_CONTEXT_ACTIONS.RENAME_WORKSPACE]: '重命名',
[DESKTOP_CONTEXT_ACTIONS.COPY_WORKSPACE_PATH]: '复制项目路径',
[DESKTOP_CONTEXT_ACTIONS.REMOVE_WORKSPACE]: '从项目列表移除',
[DESKTOP_CONTEXT_ACTIONS.PREVIEW_FILE]: '预览文件',
Expand Down
Loading
Loading