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

View File

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

View File

@ -225,6 +225,7 @@ export const conversationMethods = {
async loadConversation(conversationId, options = {}) { async loadConversation(conversationId, options = {}) {
const force = Boolean(options.force); const force = Boolean(options.force);
const preserveListPosition = Boolean(options.preserveListPosition);
debugLog('加载对话:', conversationId); debugLog('加载对话:', conversationId);
traceLog('loadConversation:start', { traceLog('loadConversation:start', {
conversationId, conversationId,
@ -358,7 +359,9 @@ export const conversationMethods = {
this.titleReady = true; this.titleReady = true;
this.suppressTitleTyping = false; this.suppressTitleTyping = false;
this.startTitleTyping(this.currentConversationTitle, { animate: false }); this.startTitleTyping(this.currentConversationTitle, { animate: false });
this.promoteConversationToTop(conversationId); if (!preserveListPosition) {
this.promoteConversationToTop(conversationId);
}
history.pushState( history.pushState(
{ conversationId }, { conversationId },
'', '',
@ -560,6 +563,10 @@ export const conversationMethods = {
if (result.success) { if (result.success) {
const newConversationId = result.conversation_id; const newConversationId = result.conversation_id;
const waitForAnimation = (durationMs) =>
new Promise((resolve) => {
window.setTimeout(resolve, durationMs);
});
debugLog('新对话创建成功:', newConversationId); debugLog('新对话创建成功:', newConversationId);
traceLog('createNewConversation:created', { newConversationId }); traceLog('createNewConversation:created', { newConversationId });
@ -571,10 +578,21 @@ export const conversationMethods = {
total_messages: 0, total_messages: 0,
total_tools: 0 total_tools: 0
}; };
this.conversationInsertAnimations = {
...this.conversationInsertAnimations,
[newConversationId]: 'create'
};
this.conversationListAnimationMode = 'create';
this.conversations = [ this.conversations = [
placeholder, placeholder,
...this.conversations.filter((conv) => conv && conv.id !== newConversationId) ...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); await this.applyPendingVersioningToConversation(newConversationId);
@ -586,6 +604,9 @@ export const conversationMethods = {
currentConversationId: this.currentConversationId currentConversationId: this.currentConversationId
}); });
// 等待顶部插入动画完成,避免刷新列表时打断“整体下移 + 从左到右出现”。
await waitForAnimation(540);
// 刷新对话列表获取最新统计 // 刷新对话列表获取最新统计
this.conversationsOffset = 0; this.conversationsOffset = 0;
await this.loadConversationsList(); await this.loadConversationsList();
@ -632,6 +653,14 @@ export const conversationMethods = {
return; return;
} }
const waitForAnimation = (durationMs) =>
new Promise((resolve) => {
window.setTimeout(resolve, durationMs);
});
// 确认弹窗自身有 250ms 退出动画;等弹窗完全消失后,再开始列表删除动画。
await waitForAnimation(270);
debugLog('删除对话:', conversationId); debugLog('删除对话:', conversationId);
this.logMessageState('deleteConversation:start', { conversationId }); this.logMessageState('deleteConversation:start', { conversationId });
@ -657,9 +686,22 @@ export const conversationMethods = {
history.replaceState({}, '', '/new'); 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; this.conversationsOffset = 0;
await this.loadConversationsList(); await this.loadConversationsList();
this.conversationListAnimationMode = 'idle';
} else { } else {
console.error('删除对话失败:', result.message); console.error('删除对话失败:', result.message);
this.uiPushToast({ this.uiPushToast({
@ -689,12 +731,58 @@ export const conversationMethods = {
if (response.ok && result.success) { if (response.ok && result.success) {
const newId = result.duplicate_conversation_id; const newId = result.duplicate_conversation_id;
const waitForAnimation = (durationMs) =>
new Promise((resolve) => {
window.setTimeout(resolve, durationMs);
});
if (newId) { 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; this.conversationInsertAnimations = {
await this.loadConversationsList(); ...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 { } else {
const message = result.message || result.error || '复制失败'; const message = result.message || result.error || '复制失败';
this.uiPushToast({ this.uiPushToast({

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,17 +13,32 @@
min-height: var(--app-viewport, 100vh) !important; min-height: var(--app-viewport, 100vh) !important;
overflow: hidden; overflow: hidden;
background: var(--claude-left-rail); background: var(--claude-left-rail);
border-right: 1px solid var(--claude-border); border-right: 0;
color: var(--claude-text); color: var(--claude-text);
transition: transition: width 320ms cubic-bezier(0.22, 1, 0.36, 1);
width 320ms cubic-bezier(0.22, 1, 0.36, 1), }
border-color 160ms ease;
.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 { .conversation-sidebar.collapsed {
width: var(--conversation-collapsed-width); width: var(--conversation-collapsed-width);
} }
.conversation-sidebar.collapsed::after {
opacity: 0;
}
.conversation-sidebar-inner { .conversation-sidebar-inner {
width: var(--conversation-expanded-width); width: var(--conversation-expanded-width);
height: 100%; height: 100%;
@ -92,6 +107,15 @@
outline: none; 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, .sidebar-nav-label,
.conversation-search, .conversation-search,
.conversation-list, .conversation-list,
@ -178,6 +202,7 @@
min-height: 0; min-height: 0;
flex: 1 1 auto; flex: 1 1 auto;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden;
padding-top: 12px; padding-top: 12px;
scrollbar-width: none !important; scrollbar-width: none !important;
-ms-overflow-style: none; -ms-overflow-style: none;
@ -189,6 +214,10 @@
display: none !important; display: none !important;
} }
.conversation-sidebar .conversation-list-items {
position: relative;
}
.conversation-sidebar .conversation-item { .conversation-sidebar .conversation-item {
position: relative; position: relative;
grid-template-columns: minmax(0, 1fr) 40px; grid-template-columns: minmax(0, 1fr) 40px;
@ -203,6 +232,89 @@
color 140ms ease; 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:hover,
.conversation-sidebar .conversation-item:focus-within, .conversation-sidebar .conversation-item:focus-within,
.conversation-sidebar .conversation-item.active:hover, .conversation-sidebar .conversation-item.active:hover,

View File

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