feat(ui): add sidebar list and workspace animations

This commit is contained in:
JOJO 2026-05-28 04:49:52 +08:00
parent 3511f0e6ee
commit ecba45da65
10 changed files with 437 additions and 210 deletions

View File

@ -343,11 +343,11 @@
></div>
</div>
<transition name="desktop-approval-fade">
<div
v-if="!isMobileViewport && !rightCollapsed"
class="desktop-approval-overlay"
>
<div class="desktop-approval-sheet" :style="{ width: Math.min(rightWidth || 420, 520) + 'px' }">
<div v-if="!isMobileViewport && !rightCollapsed" class="desktop-approval-overlay">
<div
class="desktop-approval-sheet"
:style="{ width: Math.min(rightWidth || 420, 520) + 'px' }"
>
<ToolApprovalPanel
:collapsed="false"
:width="Math.min(rightWidth || 420, 520)"

View File

@ -56,6 +56,8 @@ export const computed = {
'searchQuery',
'searchTimer',
'searchResults',
'conversationInsertAnimations',
'conversationListAnimationMode',
'searchActive',
'searchInProgress',
'searchMoreAvailable',

View File

@ -225,6 +225,7 @@ export const conversationMethods = {
async loadConversation(conversationId, options = {}) {
const force = Boolean(options.force);
const preserveListPosition = Boolean(options.preserveListPosition);
debugLog('加载对话:', conversationId);
traceLog('loadConversation:start', {
conversationId,
@ -358,7 +359,9 @@ export const conversationMethods = {
this.titleReady = true;
this.suppressTitleTyping = false;
this.startTitleTyping(this.currentConversationTitle, { animate: false });
this.promoteConversationToTop(conversationId);
if (!preserveListPosition) {
this.promoteConversationToTop(conversationId);
}
history.pushState(
{ conversationId },
'',
@ -560,6 +563,10 @@ export const conversationMethods = {
if (result.success) {
const newConversationId = result.conversation_id;
const waitForAnimation = (durationMs) =>
new Promise((resolve) => {
window.setTimeout(resolve, durationMs);
});
debugLog('新对话创建成功:', newConversationId);
traceLog('createNewConversation:created', { newConversationId });
@ -571,10 +578,21 @@ export const conversationMethods = {
total_messages: 0,
total_tools: 0
};
this.conversationInsertAnimations = {
...this.conversationInsertAnimations,
[newConversationId]: 'create'
};
this.conversationListAnimationMode = 'create';
this.conversations = [
placeholder,
...this.conversations.filter((conv) => conv && conv.id !== newConversationId)
];
window.setTimeout(() => {
const rest = { ...this.conversationInsertAnimations };
delete rest[newConversationId];
this.conversationInsertAnimations = rest;
this.conversationListAnimationMode = 'idle';
}, 700);
await this.applyPendingVersioningToConversation(newConversationId);
@ -586,6 +604,9 @@ export const conversationMethods = {
currentConversationId: this.currentConversationId
});
// 等待顶部插入动画完成,避免刷新列表时打断“整体下移 + 从左到右出现”。
await waitForAnimation(540);
// 刷新对话列表获取最新统计
this.conversationsOffset = 0;
await this.loadConversationsList();
@ -632,6 +653,14 @@ export const conversationMethods = {
return;
}
const waitForAnimation = (durationMs) =>
new Promise((resolve) => {
window.setTimeout(resolve, durationMs);
});
// 确认弹窗自身有 250ms 退出动画;等弹窗完全消失后,再开始列表删除动画。
await waitForAnimation(270);
debugLog('删除对话:', conversationId);
this.logMessageState('deleteConversation:start', { conversationId });
@ -657,9 +686,22 @@ export const conversationMethods = {
history.replaceState({}, '', '/new');
}
// 先从本地列表移除,让侧边栏执行删除动画;随后再刷新服务端列表补齐分页状态
this.conversationListAnimationMode = 'delete';
this.conversations = this.conversations.filter(
(conversation) => conversation.id !== conversationId
);
this.searchResults = this.searchResults.filter(
(conversation) => conversation.id !== conversationId
);
// 等待“左滑离场 + 0.1s 后集体上移”动画结束,避免刷新列表打断过渡。
await waitForAnimation(560);
// 刷新对话列表
this.conversationsOffset = 0;
await this.loadConversationsList();
this.conversationListAnimationMode = 'idle';
} else {
console.error('删除对话失败:', result.message);
this.uiPushToast({
@ -689,12 +731,58 @@ export const conversationMethods = {
if (response.ok && result.success) {
const newId = result.duplicate_conversation_id;
const waitForAnimation = (durationMs) =>
new Promise((resolve) => {
window.setTimeout(resolve, durationMs);
});
if (newId) {
await this.loadConversation(newId, { force: true });
}
const sourceIndex = this.conversations.findIndex(
(conv) => conv && conv.id === conversationId
);
const sourceConversation = sourceIndex >= 0 ? this.conversations[sourceIndex] : null;
const duplicateTitle =
result.load_result?.title || sourceConversation?.title || '复制的对话';
const duplicatePlaceholder = {
id: newId,
title: duplicateTitle,
updated_at: new Date().toISOString(),
total_messages: sourceConversation?.total_messages || 0,
total_tools: sourceConversation?.total_tools || 0
};
this.conversationsOffset = 0;
await this.loadConversationsList();
this.conversationInsertAnimations = {
...this.conversationInsertAnimations,
[conversationId]: 'duplicateSource',
[newId]: 'duplicate'
};
this.conversationListAnimationMode = 'duplicate';
const withoutDuplicate = this.conversations.filter((conv) => conv && conv.id !== newId);
const insertIndex = withoutDuplicate.findIndex(
(conv) => conv && conv.id === conversationId
);
if (insertIndex >= 0) {
this.conversations = [
...withoutDuplicate.slice(0, insertIndex + 1),
duplicatePlaceholder,
...withoutDuplicate.slice(insertIndex + 1)
];
} else {
this.conversations = [duplicatePlaceholder, ...withoutDuplicate];
}
window.setTimeout(() => {
const rest = { ...this.conversationInsertAnimations };
delete rest[conversationId];
delete rest[newId];
this.conversationInsertAnimations = rest;
this.conversationListAnimationMode = 'idle';
}, 860);
// 先让“目标下方钻出 + 下方整体下移”动画播完,再加载复制出的对话。
await waitForAnimation(860);
await this.loadConversation(newId, { force: true, preserveListPosition: true });
}
} else {
const message = result.message || result.error || '复制失败';
this.uiPushToast({

View File

@ -310,16 +310,12 @@ const emits = defineEmits<{
const panelMenuWrapper = ref<HTMLElement | null>(null);
const statusLogoSvg = statusLogoSvgRaw.replace(/fill="#000000"/g, 'fill="currentColor"');
const panelStyle = computed(() => {
if (props.collapsed) {
return {
width: '0px',
minWidth: '0px'
};
}
const px = `${props.width}px`;
const layoutWidth = props.collapsed ? '0px' : px;
return {
width: px,
minWidth: px
'--workspace-content-width': px,
width: layoutWidth,
minWidth: layoutWidth
};
});

View File

@ -113,12 +113,23 @@
<div v-else-if="!displayConversations.length && !loading" class="no-conversations">
{{ searchActive ? '未找到匹配对话' : '暂无对话记录' }}
</div>
<div v-else>
<transition-group
v-else
name="conversation-delete"
tag="div"
class="conversation-list-items"
:class="`animation-mode-${conversationListAnimationMode}`"
>
<div
v-for="conv in displayConversations"
:key="conv.id"
class="conversation-item"
:class="{ active: conv.id === currentConversationId }"
:class="{
active: conv.id === currentConversationId,
'insert-from-left': conversationInsertAnimations[conv.id] === 'create',
'insert-from-under': conversationInsertAnimations[conv.id] === 'duplicate',
'duplicate-source-mask': conversationInsertAnimations[conv.id] === 'duplicateSource'
}"
@click="
openActionMenuId = null;
$emit('select', conv.id);
@ -168,7 +179,7 @@
</div>
</div>
</div>
</div>
</transition-group>
<div v-if="!searchActive && hasMore" class="load-more">
<button
class="load-more-btn"
@ -281,6 +292,8 @@ const {
searchInProgress,
searchMoreAvailable,
conversations,
conversationInsertAnimations,
conversationListAnimationMode,
conversationsLoading,
hasMoreConversations: hasMore,
loadingMoreConversations: loadingMore,

View File

@ -11,6 +11,8 @@ export interface ConversationSummary {
interface ConversationState {
conversations: ConversationSummary[];
searchResults: ConversationSummary[];
conversationInsertAnimations: Record<string, 'create' | 'duplicate' | 'duplicateSource'>;
conversationListAnimationMode: 'idle' | 'create' | 'duplicate' | 'delete';
conversationsLoading: boolean;
hasMoreConversations: boolean;
loadingMoreConversations: boolean;
@ -31,6 +33,8 @@ export const useConversationStore = defineStore('conversation', {
state: (): ConversationState => ({
conversations: [],
searchResults: [],
conversationInsertAnimations: {},
conversationListAnimationMode: 'idle',
conversationsLoading: false,
hasMoreConversations: false,
loadingMoreConversations: false,
@ -50,6 +54,8 @@ export const useConversationStore = defineStore('conversation', {
resetConversations() {
this.conversations = [];
this.searchResults = [];
this.conversationInsertAnimations = {};
this.conversationListAnimationMode = 'idle';
this.searchActive = false;
this.searchInProgress = false;
this.searchMoreAvailable = false;

View File

@ -1,174 +1,174 @@
/* CSS variables + shared design tokens */
:root {
color-scheme: light;
--app-viewport: 100vh;
--app-bottom-inset: env(safe-area-inset-bottom, 0px);
--claude-bg: #eeece2;
--claude-panel: rgba(255, 255, 255, 0.82);
--claude-left-rail: #f7f3ea;
--claude-sidebar: rgba(255, 255, 255, 0.68);
--claude-border: rgba(118, 103, 84, 0.25);
--claude-border-strong: rgba(118, 103, 84, 0.35);
--claude-text: #3d3929;
--claude-text-secondary: #7f7766;
--claude-text-tertiary: #a59a86;
--claude-muted: rgba(121, 109, 94, 0.4);
--claude-accent: #da7756;
--claude-accent-strong: #bd5d3a;
--claude-deep: #f2a93b;
--claude-deep-strong: #d07a14;
--claude-highlight: rgba(218, 119, 86, 0.14);
--claude-button-hover: #c76541;
--claude-button-active: #a95331;
--claude-shadow: 0 14px 36px rgba(61, 57, 41, 0.12);
--claude-success: #76b086;
--claude-warning: #d99845;
/* Theme-neutral surfaces */
--theme-surface-card: #fffaf4;
--theme-surface-strong: #ffffff;
--theme-surface-soft: rgba(255, 255, 255, 0.92);
--theme-surface-muted: rgba(255, 255, 255, 0.85);
--theme-overlay-scrim: rgba(33, 24, 14, 0.55);
--theme-shadow-strong: 0 28px 60px rgba(38, 28, 18, 0.25);
--theme-shadow-soft: 0 12px 24px rgba(38, 28, 18, 0.12);
--theme-shadow-mid: 0 20px 45px rgba(16, 24, 40, 0.08);
--theme-control-border: rgba(118, 103, 84, 0.25);
--theme-control-border-strong: rgba(118, 103, 84, 0.35);
--theme-switch-track: #d7d1c5;
--theme-chip-bg: rgba(118, 103, 84, 0.08);
--theme-chip-border: rgba(118, 103, 84, 0.2);
--theme-badge-bg: rgba(118, 103, 84, 0.12);
--theme-tab-active: rgba(189, 93, 58, 0.12);
--theme-mobile-menu: rgba(255, 255, 255, 0.35);
--theme-mobile-menu-shadow: 0 12px 30px rgba(38, 28, 18, 0.15);
--theme-card-border-strong: rgba(118, 103, 84, 0.25);
color-scheme: light;
--app-viewport: 100vh;
--app-bottom-inset: env(safe-area-inset-bottom, 0px);
--claude-bg: #eeece2;
--claude-panel: rgba(255, 255, 255, 0.82);
--claude-left-rail: #f7f3ea;
--claude-sidebar: rgba(255, 255, 255, 0.68);
--claude-border: rgba(118, 103, 84, 0.25);
--claude-border-strong: rgba(118, 103, 84, 0.35);
--claude-text: #3d3929;
--claude-text-secondary: #7f7766;
--claude-text-tertiary: #a59a86;
--claude-muted: rgba(121, 109, 94, 0.4);
--claude-accent: #da7756;
--claude-accent-strong: #bd5d3a;
--claude-deep: #f2a93b;
--claude-deep-strong: #d07a14;
--claude-highlight: rgba(218, 119, 86, 0.14);
--claude-button-hover: #c76541;
--claude-button-active: #a95331;
--claude-shadow: 0 14px 36px rgba(61, 57, 41, 0.12);
--claude-success: #76b086;
--claude-warning: #d99845;
/* Theme-neutral surfaces */
--theme-surface-card: #fffaf4;
--theme-surface-strong: #ffffff;
--theme-surface-soft: rgba(255, 255, 255, 0.92);
--theme-surface-muted: rgba(255, 255, 255, 0.85);
--theme-overlay-scrim: rgba(33, 24, 14, 0.55);
--theme-shadow-strong: 0 28px 60px rgba(38, 28, 18, 0.25);
--theme-shadow-soft: 0 12px 24px rgba(38, 28, 18, 0.12);
--theme-shadow-mid: 0 20px 45px rgba(16, 24, 40, 0.08);
--theme-control-border: rgba(118, 103, 84, 0.25);
--theme-control-border-strong: rgba(118, 103, 84, 0.35);
--theme-switch-track: #d7d1c5;
--theme-chip-bg: rgba(118, 103, 84, 0.08);
--theme-chip-border: rgba(118, 103, 84, 0.2);
--theme-badge-bg: rgba(118, 103, 84, 0.12);
--theme-tab-active: rgba(189, 93, 58, 0.12);
--theme-mobile-menu: rgba(255, 255, 255, 0.35);
--theme-mobile-menu-shadow: 0 12px 30px rgba(38, 28, 18, 0.15);
--theme-card-border-strong: rgba(118, 103, 84, 0.25);
}
:root[data-theme='classic'] {
color-scheme: light;
--claude-bg: #eeece2;
--claude-panel: rgba(255, 255, 255, 0.82);
--claude-left-rail: #f7f3ea;
--claude-sidebar: rgba(255, 255, 255, 0.68);
--claude-border: rgba(118, 103, 84, 0.25);
--claude-border-strong: rgba(118, 103, 84, 0.35);
--claude-text: #3d3929;
--claude-text-secondary: #7f7766;
--claude-text-tertiary: #a59a86;
--claude-muted: rgba(121, 109, 94, 0.4);
--claude-accent: #da7756;
--claude-accent-strong: #bd5d3a;
--claude-deep: #f2a93b;
--claude-deep-strong: #d07a14;
--claude-highlight: rgba(218, 119, 86, 0.14);
--claude-button-hover: #c76541;
--claude-button-active: #a95331;
--claude-shadow: 0 14px 36px rgba(61, 57, 41, 0.12);
--claude-success: #76b086;
--claude-warning: #d99845;
--theme-surface-card: #fffaf4;
--theme-surface-strong: #ffffff;
--theme-surface-soft: rgba(255, 255, 255, 0.92);
--theme-surface-muted: rgba(255, 255, 255, 0.85);
--theme-overlay-scrim: rgba(33, 24, 14, 0.55);
--theme-shadow-strong: 0 28px 60px rgba(38, 28, 18, 0.25);
--theme-shadow-soft: 0 12px 24px rgba(38, 28, 18, 0.12);
--theme-shadow-mid: 0 20px 45px rgba(16, 24, 40, 0.08);
--theme-control-border: rgba(118, 103, 84, 0.25);
--theme-control-border-strong: rgba(118, 103, 84, 0.35);
--theme-switch-track: #d7d1c5;
--theme-chip-bg: rgba(118, 103, 84, 0.08);
--theme-chip-border: rgba(118, 103, 84, 0.2);
--theme-badge-bg: rgba(118, 103, 84, 0.12);
--theme-tab-active: rgba(189, 93, 58, 0.12);
--theme-mobile-menu: rgba(255, 255, 255, 0.35);
--theme-mobile-menu-shadow: 0 12px 30px rgba(38, 28, 18, 0.15);
--theme-card-border-strong: rgba(118, 103, 84, 0.25);
color-scheme: light;
--claude-bg: #eeece2;
--claude-panel: rgba(255, 255, 255, 0.82);
--claude-left-rail: #f7f3ea;
--claude-sidebar: rgba(255, 255, 255, 0.68);
--claude-border: rgba(118, 103, 84, 0.25);
--claude-border-strong: rgba(118, 103, 84, 0.35);
--claude-text: #3d3929;
--claude-text-secondary: #7f7766;
--claude-text-tertiary: #a59a86;
--claude-muted: rgba(121, 109, 94, 0.4);
--claude-accent: #da7756;
--claude-accent-strong: #bd5d3a;
--claude-deep: #f2a93b;
--claude-deep-strong: #d07a14;
--claude-highlight: rgba(218, 119, 86, 0.14);
--claude-button-hover: #c76541;
--claude-button-active: #a95331;
--claude-shadow: 0 14px 36px rgba(61, 57, 41, 0.12);
--claude-success: #76b086;
--claude-warning: #d99845;
--theme-surface-card: #fffaf4;
--theme-surface-strong: #ffffff;
--theme-surface-soft: rgba(255, 255, 255, 0.92);
--theme-surface-muted: rgba(255, 255, 255, 0.85);
--theme-overlay-scrim: rgba(33, 24, 14, 0.55);
--theme-shadow-strong: 0 28px 60px rgba(38, 28, 18, 0.25);
--theme-shadow-soft: 0 12px 24px rgba(38, 28, 18, 0.12);
--theme-shadow-mid: 0 20px 45px rgba(16, 24, 40, 0.08);
--theme-control-border: rgba(118, 103, 84, 0.25);
--theme-control-border-strong: rgba(118, 103, 84, 0.35);
--theme-switch-track: #d7d1c5;
--theme-chip-bg: rgba(118, 103, 84, 0.08);
--theme-chip-border: rgba(118, 103, 84, 0.2);
--theme-badge-bg: rgba(118, 103, 84, 0.12);
--theme-tab-active: rgba(189, 93, 58, 0.12);
--theme-mobile-menu: rgba(255, 255, 255, 0.35);
--theme-mobile-menu-shadow: 0 12px 30px rgba(38, 28, 18, 0.15);
--theme-card-border-strong: rgba(118, 103, 84, 0.25);
}
:root[data-theme='light'] {
color-scheme: light;
/* 类似 ChatGPT 的明亮配色:白底黑字,优雅浅灰 */
--claude-bg: #ffffff;
--claude-panel: #ffffff;
--claude-left-rail: #f9f9f9;
--claude-sidebar: #ffffff;
--claude-border: rgba(0, 0, 0, 0.08);
--claude-border-strong: rgba(0, 0, 0, 0.12);
--claude-text: #0d0d0d;
--claude-text-secondary: #676767;
--claude-text-tertiary: #8e8e8e;
--claude-muted: rgba(0, 0, 0, 0.3);
--claude-accent: #acacac;
--claude-accent-strong: #8e8e8e;
--claude-deep: #c4c4c4;
--claude-deep-strong: #acacac;
--claude-highlight: rgba(0, 0, 0, 0.05);
--claude-button-hover: #d1d1d1;
--claude-button-active: #acacac;
--claude-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
--claude-success: #10b981;
--claude-warning: #f59e0b;
--theme-surface-card: #ffffff;
--theme-surface-strong: #ffffff;
--theme-surface-soft: #ffffff;
--theme-surface-muted: #fafafa;
--theme-overlay-scrim: rgba(0, 0, 0, 0.5);
--theme-shadow-strong: 0 2px 4px rgba(0, 0, 0, 0.08);
--theme-shadow-soft: 0 1px 2px rgba(0, 0, 0, 0.05);
--theme-shadow-mid: 0 1px 3px rgba(0, 0, 0, 0.06);
--theme-control-border: rgba(0, 0, 0, 0.08);
--theme-control-border-strong: rgba(0, 0, 0, 0.12);
--theme-switch-track: #ececec;
--theme-chip-bg: #f5f5f5;
--theme-chip-border: rgba(0, 0, 0, 0.08);
--theme-badge-bg: #f5f5f5;
--theme-tab-active: rgba(0, 0, 0, 0.05);
--theme-mobile-menu: rgba(255, 255, 255, 0.95);
--theme-mobile-menu-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
--theme-card-border-strong: rgba(0, 0, 0, 0.1);
color-scheme: light;
/* 类似 ChatGPT 的明亮配色:白底黑字,优雅浅灰 */
--claude-bg: #ffffff;
--claude-panel: #ffffff;
--claude-left-rail: #f9f9f9;
--claude-sidebar: #ffffff;
--claude-border: rgba(0, 0, 0, 0.08);
--claude-border-strong: rgba(0, 0, 0, 0.12);
--claude-text: #0d0d0d;
--claude-text-secondary: #676767;
--claude-text-tertiary: #8e8e8e;
--claude-muted: rgba(0, 0, 0, 0.3);
--claude-accent: #acacac;
--claude-accent-strong: #8e8e8e;
--claude-deep: #c4c4c4;
--claude-deep-strong: #acacac;
--claude-highlight: rgba(0, 0, 0, 0.05);
--claude-button-hover: #d1d1d1;
--claude-button-active: #acacac;
--claude-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
--claude-success: #10b981;
--claude-warning: #f59e0b;
--theme-surface-card: #ffffff;
--theme-surface-strong: #ffffff;
--theme-surface-soft: #ffffff;
--theme-surface-muted: #fafafa;
--theme-overlay-scrim: rgba(0, 0, 0, 0.5);
--theme-shadow-strong: 0 2px 4px rgba(0, 0, 0, 0.08);
--theme-shadow-soft: 0 1px 2px rgba(0, 0, 0, 0.05);
--theme-shadow-mid: 0 1px 3px rgba(0, 0, 0, 0.06);
--theme-control-border: rgba(0, 0, 0, 0.08);
--theme-control-border-strong: rgba(0, 0, 0, 0.12);
--theme-switch-track: #ececec;
--theme-chip-bg: #f5f5f5;
--theme-chip-border: rgba(0, 0, 0, 0.08);
--theme-badge-bg: #f5f5f5;
--theme-tab-active: rgba(0, 0, 0, 0.05);
--theme-mobile-menu: rgba(255, 255, 255, 0.95);
--theme-mobile-menu-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
--theme-card-border-strong: rgba(0, 0, 0, 0.1);
}
:root[data-theme='dark'] {
color-scheme: dark;
/* 暗色主题:灰黑色底色,白色文字 */
--claude-bg: #1a1a1a;
--claude-panel: #1a1a1a;
--claude-left-rail: #0a0a0a;
--claude-sidebar: #0a0a0a;
--claude-border: rgba(255, 255, 255, 0.08);
--claude-border-strong: rgba(255, 255, 255, 0.12);
--claude-text: #ffffff;
--claude-text-secondary: #a0a0a0;
--claude-text-tertiary: #707070;
--claude-muted: rgba(255, 255, 255, 0.3);
--claude-accent: #606060;
--claude-accent-strong: #505050;
--claude-deep: #808080;
--claude-deep-strong: #606060;
--claude-highlight: rgba(255, 255, 255, 0.05);
--claude-button-hover: #707070;
--claude-button-active: #505050;
--claude-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
--claude-success: #10b981;
--claude-warning: #f59e0b;
--theme-surface-card: #0a0a0a;
--theme-surface-strong: #1a1a1a;
--theme-surface-soft: #0f0f0f;
--theme-surface-muted: #141414;
--theme-overlay-scrim: rgba(0, 0, 0, 0.8);
--theme-shadow-strong: 0 4px 12px rgba(0, 0, 0, 0.6);
--theme-shadow-soft: 0 2px 6px rgba(0, 0, 0, 0.4);
--theme-shadow-mid: 0 3px 8px rgba(0, 0, 0, 0.5);
--theme-control-border: rgba(255, 255, 255, 0.08);
--theme-control-border-strong: rgba(255, 255, 255, 0.12);
--theme-switch-track: #2a2a2a;
--theme-chip-bg: #2a2a2a;
--theme-chip-border: rgba(255, 255, 255, 0.08);
--theme-badge-bg: #2a2a2a;
--theme-tab-active: rgba(255, 255, 255, 0.05);
--theme-mobile-menu: rgba(10, 10, 10, 0.95);
--theme-mobile-menu-shadow: 0 4px 12px rgba(0, 0, 0, 0.6);
--theme-card-border-strong: rgba(255, 255, 255, 0.1);
color-scheme: dark;
/* 暗色主题:灰黑色底色,白色文字 */
--claude-bg: #1a1a1a;
--claude-panel: #1a1a1a;
--claude-left-rail: #181818;
--claude-sidebar: #181818;
--claude-border: rgba(255, 255, 255, 0.08);
--claude-border-strong: rgba(255, 255, 255, 0.12);
--claude-text: #ffffff;
--claude-text-secondary: #a0a0a0;
--claude-text-tertiary: #707070;
--claude-muted: rgba(255, 255, 255, 0.3);
--claude-accent: #606060;
--claude-accent-strong: #505050;
--claude-deep: #808080;
--claude-deep-strong: #606060;
--claude-highlight: rgba(255, 255, 255, 0.05);
--claude-button-hover: #707070;
--claude-button-active: #505050;
--claude-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
--claude-success: #10b981;
--claude-warning: #f59e0b;
--theme-surface-card: #0a0a0a;
--theme-surface-strong: #1a1a1a;
--theme-surface-soft: #0f0f0f;
--theme-surface-muted: #141414;
--theme-overlay-scrim: rgba(0, 0, 0, 0.8);
--theme-shadow-strong: 0 4px 12px rgba(0, 0, 0, 0.6);
--theme-shadow-soft: 0 2px 6px rgba(0, 0, 0, 0.4);
--theme-shadow-mid: 0 3px 8px rgba(0, 0, 0, 0.5);
--theme-control-border: rgba(255, 255, 255, 0.08);
--theme-control-border-strong: rgba(255, 255, 255, 0.12);
--theme-switch-track: #2a2a2a;
--theme-chip-bg: #2a2a2a;
--theme-chip-border: rgba(255, 255, 255, 0.08);
--theme-badge-bg: #2a2a2a;
--theme-tab-active: rgba(255, 255, 255, 0.05);
--theme-mobile-menu: rgba(10, 10, 10, 0.95);
--theme-mobile-menu-shadow: 0 4px 12px rgba(0, 0, 0, 0.6);
--theme-card-border-strong: rgba(255, 255, 255, 0.1);
}

View File

@ -32,32 +32,36 @@
flex-direction: column;
transition:
width 0.45s cubic-bezier(0.4, 0, 0.2, 1),
min-width 0.45s cubic-bezier(0.4, 0, 0.2, 1),
opacity 0.35s ease,
transform 0.45s cubic-bezier(0.4, 0, 0.2, 1);
min-width 0.45s cubic-bezier(0.4, 0, 0.2, 1);
}
.workspace-panel {
flex: 0 0 auto;
will-change: width, transform, opacity;
overflow: hidden;
will-change: width, min-width;
transition:
width 0.45s cubic-bezier(0.4, 0, 0.2, 1),
min-width 0.45s cubic-bezier(0.4, 0, 0.2, 1),
opacity 0.35s ease,
transform 0.45s cubic-bezier(0.4, 0, 0.2, 1);
min-width 0.45s cubic-bezier(0.4, 0, 0.2, 1);
}
.workspace-panel > * {
width: var(--workspace-content-width);
min-width: var(--workspace-content-width);
transition:
opacity 0.32s ease,
transform 0.34s cubic-bezier(0.22, 1, 0.36, 1);
will-change: transform, opacity;
}
.workspace-panel--collapsed {
flex: 0 0 0 !important;
width: 0 !important;
min-width: 0 !important;
max-width: 0 !important;
opacity: 0;
transform: translateY(-70px);
overflow: hidden;
pointer-events: none;
}
.workspace-panel--collapsed > * {
opacity: 0;
transform: translateY(-28px);
}
.sidebar-status {
padding: 18px 18px 8px;
}
@ -686,8 +690,14 @@
}
[data-theme='dark'] .host-workspace-card {
background: #1f1f1f;
&:hover:not(:disabled) {
background: #222222;
}
&.active {
background: rgba(255, 255, 255, 0.08);
background: #242424;
}
}

View File

@ -13,17 +13,32 @@
min-height: var(--app-viewport, 100vh) !important;
overflow: hidden;
background: var(--claude-left-rail);
border-right: 1px solid var(--claude-border);
border-right: 0;
color: var(--claude-text);
transition:
width 320ms cubic-bezier(0.22, 1, 0.36, 1),
border-color 160ms ease;
transition: width 320ms cubic-bezier(0.22, 1, 0.36, 1);
}
.conversation-sidebar::after {
content: '';
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 1px;
pointer-events: none;
opacity: 1;
background: var(--claude-border);
transition: opacity 320ms cubic-bezier(0.22, 1, 0.36, 1);
}
.conversation-sidebar.collapsed {
width: var(--conversation-collapsed-width);
}
.conversation-sidebar.collapsed::after {
opacity: 0;
}
.conversation-sidebar-inner {
width: var(--conversation-expanded-width);
height: 100%;
@ -92,6 +107,15 @@
outline: none;
}
.conversation-sidebar:not(.collapsed) .workspace-control-btn.active {
background: transparent;
}
.conversation-sidebar:not(.collapsed) .workspace-control-btn.active:hover,
.conversation-sidebar:not(.collapsed) .workspace-control-btn.active:focus-visible {
background: var(--theme-tab-active, rgba(0, 0, 0, 0.055));
}
.sidebar-nav-label,
.conversation-search,
.conversation-list,
@ -178,6 +202,7 @@
min-height: 0;
flex: 1 1 auto;
overflow-y: auto;
overflow-x: hidden;
padding-top: 12px;
scrollbar-width: none !important;
-ms-overflow-style: none;
@ -189,6 +214,10 @@
display: none !important;
}
.conversation-sidebar .conversation-list-items {
position: relative;
}
.conversation-sidebar .conversation-item {
position: relative;
grid-template-columns: minmax(0, 1fr) 40px;
@ -203,6 +232,89 @@
color 140ms ease;
}
.conversation-sidebar .conversation-delete-move {
transition: transform 300ms cubic-bezier(0.22, 1, 0.36, 1);
}
.conversation-sidebar .animation-mode-delete .conversation-delete-move {
transition: transform 300ms cubic-bezier(0.22, 1, 0.36, 1) 100ms;
}
.conversation-sidebar .conversation-delete-enter-active {
transition:
transform 420ms cubic-bezier(0.22, 1, 0.36, 1),
opacity 260ms ease;
}
.conversation-sidebar
.animation-mode-duplicate
.conversation-delete-enter-active.insert-from-under {
transition-delay: 300ms;
}
.conversation-sidebar .conversation-delete-enter-active.insert-from-under {
z-index: 0;
}
.conversation-sidebar .conversation-delete-enter-from.insert-from-left {
transform: translateX(-112%);
opacity: 0;
}
.conversation-sidebar .conversation-delete-enter-from.insert-from-under {
transform: translateY(-100%);
opacity: 0;
}
.conversation-sidebar .conversation-item.duplicate-source-mask {
z-index: 2;
}
.conversation-sidebar .conversation-item.duplicate-source-mask::after {
content: '';
position: absolute;
inset: 0;
z-index: 1;
border-radius: 12px;
background: var(--claude-left-rail);
pointer-events: none;
}
.conversation-sidebar .conversation-item.duplicate-source-mask:hover::after {
background: var(--theme-tab-active, rgba(0, 0, 0, 0.055));
}
:root[data-theme='dark']
.conversation-sidebar
.conversation-item.duplicate-source-mask:hover::after,
body[data-theme='dark']
.conversation-sidebar
.conversation-item.duplicate-source-mask:hover::after {
background: rgba(255, 255, 255, 0.078);
}
.conversation-sidebar .conversation-item.duplicate-source-mask > * {
position: relative;
z-index: 2;
}
.conversation-sidebar .conversation-delete-leave-active {
position: absolute;
left: 0;
right: 0;
width: 100%;
z-index: 1;
pointer-events: none;
transition:
transform 420ms cubic-bezier(0.32, 0, 0.67, 0),
opacity 260ms ease;
}
.conversation-sidebar .conversation-delete-leave-to {
transform: translateX(-112%);
opacity: 0;
}
.conversation-sidebar .conversation-item:hover,
.conversation-sidebar .conversation-item:focus-within,
.conversation-sidebar .conversation-item.active:hover,

View File

@ -1,15 +1,15 @@
/* 主体容器,让三栏布局占满视口 */
.main-container {
position: relative;
display: flex;
flex-direction: row;
height: var(--app-viewport, 100vh);
align-items: stretch;
background: var(--claude-bg);
color: var(--claude-text);
position: relative;
display: flex;
flex-direction: row;
height: var(--app-viewport, 100vh);
align-items: stretch;
background: var(--claude-bg);
color: var(--claude-text);
}
#app {
min-height: var(--app-viewport, 100vh);
background: var(--claude-bg);
min-height: var(--app-viewport, 100vh);
background: var(--claude-bg);
}