Skip to content

Commit 2f69232

Browse files
gaoconggitgithub-actions[bot]claude
authored
feat: Add collapse/expand functionality to file list panel with improved layout handling (#72)
* feat: Add collapse/expand functionality to file list panel - Added toggle button (◀) in file list header for collapsing/expanding - Implemented smooth slide animation using CSS transforms - Added minimized restore button (▶) on left side when collapsed - State persistence using localStorage (fileListCollapsed) - Full dark/light theme support with styled buttons - Responsive design - hidden on mobile devices - Follows same pattern as existing output panel toggle Resolves #70 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: 小小高 <gaoconggit@users.noreply.github.com> * feat: Improve file list collapse/expand behavior and container width calculations - Enhanced container width calculation to properly handle collapsed file list state - Added fileListResizer integration to sync width updates when toggling collapse state - Fixed issue where minimized button visibility wasn't properly managed - Improved responsive layout calculations for desktop mode with collapsed file list - Added proper handling of vertical output panel width in container calculations - Added COLLAPSED_FILE_LIST_OFFSET constant for consistent collapsed state offset 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: 小小高 <gaoconggit@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent d79384b commit 2f69232

4 files changed

Lines changed: 217 additions & 42 deletions

File tree

‎SharpPad/wwwroot/fileSystem/fileManager.js‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { showNotification, DEFAULT_CODE, PROJECT_TYPE_CHANGE_EVENT } from '../utils/common.js';
33
import { customPrompt, customConfirm } from '../utils/customPrompt.js';
44
import desktopBridge from '../utils/desktopBridge.js';
5+
import { fileListResizer } from './fileListResizer.js';
56

67
class FileManager {
78
constructor() {
@@ -44,10 +45,63 @@ class FileManager {
4445
addFolderBtn.addEventListener('click', () => this.addFolder());
4546
}
4647

48+
// 文件列表折叠/展开按钮监听
49+
const toggleFileListBtn = document.getElementById('toggleFileList');
50+
if (toggleFileListBtn) {
51+
toggleFileListBtn.addEventListener('click', () => this.toggleFileList());
52+
}
53+
54+
// 恢复文件列表按钮监听
55+
const restoreFileListBtn = document.querySelector('.restore-filelist');
56+
if (restoreFileListBtn) {
57+
restoreFileListBtn.addEventListener('click', () => this.toggleFileList());
58+
}
59+
60+
// 初始化文件列表状态
61+
this.initializeFileListState();
62+
4763
// 初始化右键菜单事件
4864
this.initializeContextMenus();
4965
}
5066

67+
initializeFileListState() {
68+
const fileList = document.getElementById('fileList');
69+
const minimizedButton = document.querySelector('.minimized-filelist-button');
70+
const isCollapsed = localStorage.getItem('fileListCollapsed') === 'true';
71+
72+
if (isCollapsed && fileList && minimizedButton) {
73+
fileList.classList.add('collapsed');
74+
minimizedButton.style.display = 'block';
75+
} else if (minimizedButton) {
76+
minimizedButton.style.display = 'none';
77+
}
78+
79+
if (fileListResizer && typeof fileListResizer.updateContainerWidth === 'function') {
80+
fileListResizer.updateContainerWidth();
81+
}
82+
}
83+
84+
toggleFileList() {
85+
const fileList = document.getElementById('fileList');
86+
const minimizedButton = document.querySelector('.minimized-filelist-button');
87+
88+
if (!fileList || !minimizedButton) return;
89+
90+
const isCollapsed = fileList.classList.toggle('collapsed');
91+
92+
if (isCollapsed) {
93+
minimizedButton.style.display = 'block';
94+
localStorage.setItem('fileListCollapsed', 'true');
95+
} else {
96+
minimizedButton.style.display = 'none';
97+
localStorage.setItem('fileListCollapsed', 'false');
98+
}
99+
100+
if (fileListResizer && typeof fileListResizer.updateContainerWidth === 'function') {
101+
fileListResizer.updateContainerWidth();
102+
}
103+
}
104+
51105
normalizeProjectType(projectType) {
52106
const fallback = 'console';
53107
const candidate = typeof projectType === 'string'

‎SharpPad/wwwroot/index.html‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
<body class="theme-dark">
2626
<div id="fileList" style="max-width: 800px; width: 380px; min-width: 340px;">
2727
<div class="file-list-header">
28+
<button id="toggleFileList" title="收起/展开">◀</button>
2829
<input type="text" id="fileFilter" placeholder="搜索文件...">
2930
<div style="display: flex; gap: 5px;">
3031
<button id="addFolderBtn" title="新建文件夹">📁</button>
@@ -34,6 +35,11 @@
3435
<ul id="fileListItems">
3536
</ul>
3637
</div>
38+
<div class="minimized-filelist-button" style="display: none;">
39+
<button class="icon-button restore-filelist" title="文件列表">
40+
<span style="transform: rotate(180deg);">▶</span>
41+
</button>
42+
</div>
3743
<div id="container">
3844
<button class="editor-control-button theme-button" id="themeButton" title="切换主题"></button>
3945
<div class="button-container">

‎SharpPad/wwwroot/styles/fileList.css‎

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,18 @@
1010
left: 0;
1111
top: 0;
1212
height: calc(100vh - 200px);
13-
transition: width 0.2s cubic-bezier(0.4, 0, 0.2, 1), height 0.2s cubic-bezier(0.4, 0, 0.2, 1);
14-
will-change: width, height;
13+
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1),
14+
height 0.3s cubic-bezier(0.4, 0, 0.2, 1),
15+
transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
16+
will-change: width, height, transform;
1517
display: flex;
1618
flex-direction: column;
1719
}
1820

21+
#fileList.collapsed {
22+
transform: translateX(-100%);
23+
}
24+
1925
/* 亮色主题样式 */
2026
body.theme-light #fileList {
2127
background-color: #f5f5f5;
@@ -369,8 +375,87 @@ body.theme-light .sort-button:hover {
369375
color: #0066b8;
370376
}
371377

378+
/* 文件列表折叠/展开按钮 */
379+
#toggleFileList {
380+
background: none;
381+
border: none;
382+
color: #cccccc;
383+
cursor: pointer;
384+
padding: 4px 8px;
385+
font-size: 14px;
386+
transition: all 0.2s;
387+
border-radius: 3px;
388+
display: flex;
389+
align-items: center;
390+
justify-content: center;
391+
opacity: 0.7;
392+
}
393+
394+
#toggleFileList:hover {
395+
background-color: rgba(255, 255, 255, 0.1);
396+
opacity: 1;
397+
}
398+
399+
body.theme-light #toggleFileList {
400+
color: #424242;
401+
}
402+
403+
body.theme-light #toggleFileList:hover {
404+
background-color: rgba(0, 0, 0, 0.05);
405+
}
406+
407+
/* 最小化文件列表按钮 */
408+
.minimized-filelist-button {
409+
position: fixed;
410+
left: 10px;
411+
top: 50%;
412+
transform: translateY(-50%);
413+
z-index: 1000;
414+
}
415+
416+
.minimized-filelist-button .icon-button {
417+
background: linear-gradient(135deg, #2979ff 0%, #1565c0 100%);
418+
border: none;
419+
color: #ffffff;
420+
cursor: pointer;
421+
padding: 10px 12px;
422+
border-radius: 0 6px 6px 0;
423+
font-size: 16px;
424+
box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.3);
425+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
426+
display: flex;
427+
align-items: center;
428+
justify-content: center;
429+
min-width: 36px;
430+
min-height: 36px;
431+
}
432+
433+
.minimized-filelist-button .icon-button:hover {
434+
background: linear-gradient(135deg, #2962ff 0%, #1976d2 100%);
435+
transform: translateX(5px);
436+
box-shadow: 3px 3px 12px rgba(0, 0, 0, 0.4);
437+
}
438+
439+
.minimized-filelist-button .icon-button:active {
440+
transform: translateX(3px);
441+
}
442+
443+
body.theme-light .minimized-filelist-button .icon-button {
444+
background: linear-gradient(135deg, #0066b8 0%, #005a9e 100%);
445+
box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.2);
446+
}
447+
448+
body.theme-light .minimized-filelist-button .icon-button:hover {
449+
background: linear-gradient(135deg, #005a9e 0%, #004b87 100%);
450+
box-shadow: 3px 3px 12px rgba(0, 0, 0, 0.3);
451+
}
452+
372453
@media only screen and (max-width: 768px) {
373454
#fileList {
374455
display: none;
375456
}
457+
458+
.minimized-filelist-button {
459+
display: none !important;
460+
}
376461
}

‎SharpPad/wwwroot/utils/common.js‎

Lines changed: 70 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { extractDirectiveReferences, buildMultiFileContext } from './multiFileHelper.js';
1+
import { extractDirectiveReferences, buildMultiFileContext } from './multiFileHelper.js';
2+
3+
const COLLAPSED_FILE_LIST_OFFSET = 48;
24

35
export const PROJECT_TYPE_CHANGE_EVENT = 'sharppad:projectTypeChanged';
46

@@ -200,45 +202,73 @@ export function getResponsiveSize(defaultSize, mobileSize) {
200202
}
201203

202204
// 响应式设置container宽度
203-
export function setContainerWidth(container, fileListWidth, chatPanelWidth, chatPanelVisible) {
204-
// 使用媒体查询检测移动设备
205-
const isMobile = window.matchMedia('(max-width: 768px) and (pointer: coarse), (max-width: 480px)').matches;
206-
207-
if (isMobile) {
208-
// 移动设备适配
209-
if (chatPanelVisible) {
210-
// 移动设备上聊天面板设置为全屏宽度,主容器隐藏或调整到最小
211-
container.style.width = '100%';
212-
container.style.marginRight = '0';
213-
214-
// 根据聊天面板的位置决定编辑器的样式
215-
// 如果聊天面板在屏幕下半部分
216-
container.style.height = 'calc(50vh - 1px)';
217-
container.style.marginBottom = '0';
218-
} else {
219-
// 聊天面板不可见时,编辑器占满全屏
220-
container.style.width = '100%';
221-
container.style.height = '100vh';
222-
container.style.marginRight = '0';
223-
container.style.marginBottom = '0';
224-
}
225-
226-
// 在移动设备上,文件列表可能会是覆盖式的,而不是并排
227-
container.style.marginLeft = '0';
228-
} else {
229-
// 桌面设备使用原来的布局逻辑
230-
if (chatPanelVisible) {
231-
container.style.width = `calc(100% - ${fileListWidth}px - ${chatPanelWidth}px)`;
232-
container.style.marginRight = `${chatPanelWidth}px`;
233-
} else {
234-
container.style.width = `calc(100% - ${fileListWidth}px)`;
235-
container.style.marginRight = '0';
236-
}
237-
container.style.marginLeft = `${fileListWidth}px`;
238-
container.style.height = '100vh';
239-
}
240-
241-
// 重新布局编辑器
205+
export function setContainerWidth(container, fileListWidth, chatPanelWidth, chatPanelVisible) {
206+
if (!container) {
207+
return;
208+
}
209+
210+
const fileListElement = document.getElementById('fileList');
211+
const isFileListCollapsed = fileListElement?.classList.contains('collapsed');
212+
const normalizedFileListWidth = Number.isFinite(fileListWidth) ? fileListWidth : 0;
213+
const effectiveFileListWidth = isFileListCollapsed ? 0 : normalizedFileListWidth;
214+
215+
// 使用媒体查询检测移动设备
216+
const isMobile = window.matchMedia('(max-width: 768px) and (pointer: coarse), (max-width: 480px)').matches;
217+
const collapsedOffset = isFileListCollapsed && !isMobile ? COLLAPSED_FILE_LIST_OFFSET : 0;
218+
219+
if (isMobile) {
220+
// 移动设备适配
221+
if (chatPanelVisible) {
222+
// 移动设备上聊天面板设置为全屏宽度,主容器隐藏或调整到最小
223+
container.style.width = '100%';
224+
container.style.marginRight = '0';
225+
226+
// 根据聊天面板的位置决定编辑器的样式
227+
// 如果聊天面板在屏幕下半部分
228+
container.style.height = 'calc(50vh - 1px)';
229+
container.style.marginBottom = '0';
230+
} else {
231+
// 聊天面板不可见时,编辑器占满全屏
232+
container.style.width = '100%';
233+
container.style.height = '100vh';
234+
container.style.marginRight = '0';
235+
container.style.marginBottom = '0';
236+
}
237+
238+
// 在移动设备上,文件列表可能会是覆盖式的,而不是并排
239+
container.style.marginLeft = '0';
240+
container.style.marginRight = '0';
241+
} else {
242+
// 桌面设备使用原来的布局逻辑
243+
const totalFileListOffset = effectiveFileListWidth + collapsedOffset;
244+
const normalizedChatWidth = chatPanelVisible && Number.isFinite(chatPanelWidth)
245+
? chatPanelWidth
246+
: 0;
247+
248+
let outputPanelWidth = 0;
249+
const outputPanel = document.getElementById('outputPanel');
250+
if (outputPanel) {
251+
const outputStyles = window.getComputedStyle(outputPanel);
252+
const isVerticalOutput = outputPanel.classList.contains('vertical') && outputStyles.display !== 'none';
253+
if (isVerticalOutput) {
254+
const parsedWidth = parseInt(outputStyles.width, 10);
255+
if (Number.isFinite(parsedWidth)) {
256+
outputPanelWidth = parsedWidth;
257+
}
258+
}
259+
}
260+
261+
// 考虑垂直输出面板的宽度,避免折叠文件列表时挤压输出区域
262+
const totalRightOffset = normalizedChatWidth + outputPanelWidth;
263+
const rightOffsetTerm = totalRightOffset > 0 ? ` - ${totalRightOffset}px` : '';
264+
265+
container.style.width = `calc(100% - ${totalFileListOffset}px${rightOffsetTerm})`;
266+
container.style.marginRight = `${totalRightOffset}px`;
267+
container.style.marginLeft = `${totalFileListOffset}px`;
268+
container.style.height = '100vh';
269+
}
270+
271+
// 重新布局编辑器
242272
layoutEditor();
243273
}
244274

0 commit comments

Comments
 (0)