From deb50dfde26076328f1916e8c6d4e9581d1adaad Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Wed, 31 Dec 2025 18:24:10 +0000 Subject: [PATCH] feat(workspace): add resizable file tree and terminal panes with draggable handles and public layout APIs --- changelog.md | 10 + ts_web/00_commitinfo_data.ts | 2 +- .../dees-workspace/dees-workspace.ts | 431 +++++++++++++----- 3 files changed, 326 insertions(+), 117 deletions(-) diff --git a/changelog.md b/changelog.md index 527c1ff..78a78a4 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,15 @@ # Changelog +## 2025-12-31 - 3.23.0 - feat(workspace) +add resizable file tree and terminal panes with draggable handles and public layout APIs + +- Introduce reactive state for currentFileTreeWidth, currentTerminalHeight, isDraggingFileTree and isDraggingTerminal +- Add mouse event handlers (mousedown/move/up) to drag-resize the file tree and terminal with min/max clamping +- Dispatch window resize event after resizing to notify Monaco/editor of layout changes +- Clean up resize event listeners in disconnectedCallback +- Initialize current sizes from component properties in firstUpdated +- Expose public layout methods: setFileTreeWidth, setTerminalHeight, resetLayout + ## 2025-12-31 - 3.22.0 - feat(workspace) add resizable markdown editor/preview split with draggable handle and markdown outlet styling/demo diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index ffcf0b2..3ca497a 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@design.estate/dees-catalog', - version: '3.22.0', + version: '3.23.0', description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.' } diff --git a/ts_web/elements/00group-workspace/dees-workspace/dees-workspace.ts b/ts_web/elements/00group-workspace/dees-workspace/dees-workspace.ts index b7aaa3c..6001df8 100644 --- a/ts_web/elements/00group-workspace/dees-workspace/dees-workspace.ts +++ b/ts_web/elements/00group-workspace/dees-workspace/dees-workspace.ts @@ -262,6 +262,19 @@ testSmartPromise(); @state() accessor initOutput: string[] = []; + // Resize state + @state() + accessor currentFileTreeWidth: number = 250; + + @state() + accessor currentTerminalHeight: number = 200; + + @state() + accessor isDraggingFileTree: boolean = false; + + @state() + accessor isDraggingTerminal: boolean = false; + // Keyboard shortcut handler (bound for proper cleanup) private keydownHandler = (e: KeyboardEvent) => { // Cmd+S (Mac) or Ctrl+S (Windows/Linux) - Save @@ -277,6 +290,71 @@ testSmartPromise(); } }; + // ========== Filetree Resize Handlers ========== + private handleFileTreeMouseDown = (e: MouseEvent) => { + e.preventDefault(); + this.isDraggingFileTree = true; + document.addEventListener('mousemove', this.handleFileTreeMouseMove); + document.addEventListener('mouseup', this.handleFileTreeMouseUp); + }; + + private handleFileTreeMouseMove = (e: MouseEvent) => { + if (!this.isDraggingFileTree) return; + + const containerRect = this.getBoundingClientRect(); + const mouseX = e.clientX - containerRect.left; + + // Clamp to min/max (150px min, 50% of container max) + const minWidth = 150; + const maxWidth = containerRect.width * 0.5; + const newWidth = Math.max(minWidth, Math.min(maxWidth, mouseX)); + + this.currentFileTreeWidth = newWidth; + }; + + private handleFileTreeMouseUp = () => { + this.isDraggingFileTree = false; + document.removeEventListener('mousemove', this.handleFileTreeMouseMove); + document.removeEventListener('mouseup', this.handleFileTreeMouseUp); + + // Notify Monaco editor of size change + window.dispatchEvent(new Event('resize')); + }; + + // ========== Terminal Resize Handlers ========== + private handleTerminalMouseDown = (e: MouseEvent) => { + e.preventDefault(); + this.isDraggingTerminal = true; + document.addEventListener('mousemove', this.handleTerminalMouseMove); + document.addEventListener('mouseup', this.handleTerminalMouseUp); + }; + + private handleTerminalMouseMove = (e: MouseEvent) => { + if (!this.isDraggingTerminal) return; + + const containerRect = this.getBoundingClientRect(); + const mouseY = e.clientY - containerRect.top; + + // Calculate terminal height from bottom + const terminalHeight = containerRect.height - mouseY; + + // Clamp to min/max (100px min, 70% of container max) + const minHeight = 100; + const maxHeight = containerRect.height * 0.7; + const newHeight = Math.max(minHeight, Math.min(maxHeight, terminalHeight)); + + this.currentTerminalHeight = newHeight; + }; + + private handleTerminalMouseUp = () => { + this.isDraggingTerminal = false; + document.removeEventListener('mousemove', this.handleTerminalMouseMove); + document.removeEventListener('mouseup', this.handleTerminalMouseUp); + + // Notify Monaco editor of size change + window.dispatchEvent(new Event('resize')); + }; + public static styles = [ themeDefaultStyles, cssManager.defaultStyles, @@ -293,67 +371,45 @@ testSmartPromise(); } .workspace-container { - display: grid; + display: flex; + flex-direction: row; height: 100%; width: 100%; } - .workspace-container.with-filetree.with-terminal { - grid-template-columns: auto 1fr; - grid-template-rows: 1fr auto; - grid-template-areas: - "filetree editor" - "filetree terminal"; - } - - .workspace-container.with-filetree:not(.with-terminal) { - grid-template-columns: auto 1fr; - grid-template-rows: 1fr; - grid-template-areas: "filetree editor"; - } - - .workspace-container:not(.with-filetree).with-terminal { - grid-template-columns: 1fr; - grid-template-rows: 1fr auto; - grid-template-areas: - "editor" - "terminal"; - } - - .workspace-container:not(.with-filetree):not(.with-terminal) { - grid-template-columns: 1fr; - grid-template-rows: 1fr; - grid-template-areas: "editor"; + .editor-area { + display: flex; + flex-direction: column; + flex: 1; + min-width: 0; + overflow: hidden; } .filetree-panel { - grid-area: filetree; position: relative; - border-right: 1px solid ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(0 0% 15%)')}; overflow: hidden; - transition: width 0.2s ease; + flex-shrink: 0; } .filetree-panel.collapsed { width: 0 !important; - border-right: none; } .editor-panel { - grid-area: editor; position: relative; display: flex; flex-direction: column; overflow: hidden; background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')}; + flex: 1; + min-width: 200px; } .terminal-panel { - grid-area: terminal; position: relative; border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(0 0% 15%)')}; overflow: hidden; - transition: height 0.2s ease; + flex-shrink: 0; } .terminal-panel.collapsed { @@ -695,14 +751,96 @@ testSmartPromise(); right: 0; bottom: 0; } + + /* Resize handles */ + .resize-handle-vertical { + width: 6px; + cursor: col-resize; + background: transparent; + transition: background 0.15s ease; + position: relative; + flex-shrink: 0; + z-index: 10; + } + + .resize-handle-vertical:hover, + .resize-handle-vertical.dragging { + background: ${cssManager.bdTheme('#3b82f6', '#58a6ff')}; + } + + .resize-handle-vertical::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 2px; + height: 32px; + background: ${cssManager.bdTheme('#9ca3af', '#6e7681')}; + border-radius: 1px; + opacity: 0; + transition: opacity 0.15s ease; + } + + .resize-handle-vertical:hover::after, + .resize-handle-vertical.dragging::after { + opacity: 1; + background: ${cssManager.bdTheme('#ffffff', '#ffffff')}; + } + + .resize-handle-horizontal { + height: 6px; + cursor: row-resize; + background: transparent; + transition: background 0.15s ease; + position: relative; + flex-shrink: 0; + z-index: 10; + } + + .resize-handle-horizontal:hover, + .resize-handle-horizontal.dragging { + background: ${cssManager.bdTheme('#3b82f6', '#58a6ff')}; + } + + .resize-handle-horizontal::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 32px; + height: 2px; + background: ${cssManager.bdTheme('#9ca3af', '#6e7681')}; + border-radius: 1px; + opacity: 0; + transition: opacity 0.15s ease; + } + + .resize-handle-horizontal:hover::after, + .resize-handle-horizontal.dragging::after { + opacity: 1; + background: ${cssManager.bdTheme('#ffffff', '#ffffff')}; + } + + /* Prevent text selection while dragging */ + .workspace-container.dragging { + user-select: none; + } + + .workspace-container.dragging .filetree-panel, + .workspace-container.dragging .editor-panel, + .workspace-container.dragging .terminal-panel { + pointer-events: none; + } + `, ]; public render(): TemplateResult { const containerClasses = [ 'workspace-container', - this.showFileTree && !this.isFileTreeCollapsed ? 'with-filetree' : '', - this.showTerminal ? 'with-terminal' : '', + (this.isDraggingFileTree || this.isDraggingTerminal) ? 'dragging' : '', ].filter(Boolean).join(' '); if (this.isInitializing) { @@ -720,10 +858,11 @@ testSmartPromise(); return html`
+ ${this.showFileTree ? html`
+ ${!this.isFileTreeCollapsed ? html` +
+ ` : ''} ` : ''} -
-
-
- ${this.openFiles.map(file => html` -
this.activateFile(file.path)} - > - ${file.modified ? html`` : ''} - ${file.name} - this.closeFile(e, file.path)}> - - -
- `)} -
-
- -
-
-
- ${this.openFiles.length === 0 ? html` -
- - Select a file to edit + +
+
+
+
+ ${this.openFiles.map(file => html` +
this.activateFile(file.path)} + > + ${file.modified ? html`` : ''} + ${file.name} + this.closeFile(e, file.path)}> + + +
+ `)}
- ` : html` - - `} +
+ +
+
+
+ ${this.openFiles.length === 0 ? html` +
+ + Select a file to edit +
+ ` : html` + + `} +
+ + + ${this.showTerminal && !this.isTerminalCollapsed ? html` +
+ ` : ''} + + + ${this.showTerminal ? html` +
+
+
+
this.activeBottomPanel = 'terminal'} + > + + Terminal +
+
this.activeBottomPanel = 'problems'} + > + + Problems + ${this.diagnosticMarkers.length > 0 ? html` + ${this.diagnosticMarkers.length} + ` : ''} +
+
+
+
+ +
+
+
+
+ +
+
+ ${this.renderProblemsPanel()} +
+
+ ` : ''}
- - ${this.showTerminal ? html` -
-
-
-
this.activeBottomPanel = 'terminal'} - > - - Terminal -
-
this.activeBottomPanel = 'problems'} - > - - Problems - ${this.diagnosticMarkers.length > 0 ? html` - ${this.diagnosticMarkers.length} - ` : ''} -
-
-
-
- -
-
-
-
- -
-
- ${this.renderProblemsPanel()} -
-
- ` : ''}
`; } @@ -827,6 +984,13 @@ testSmartPromise(); async disconnectedCallback() { await super.disconnectedCallback(); document.removeEventListener('keydown', this.keydownHandler); + + // Clean up resize event listeners + document.removeEventListener('mousemove', this.handleFileTreeMouseMove); + document.removeEventListener('mouseup', this.handleFileTreeMouseUp); + document.removeEventListener('mousemove', this.handleTerminalMouseMove); + document.removeEventListener('mouseup', this.handleTerminalMouseUp); + if (this.autoSaveInterval) { clearInterval(this.autoSaveInterval); this.autoSaveInterval = null; @@ -835,6 +999,10 @@ testSmartPromise(); } public async firstUpdated() { + // Initialize current sizes from properties + this.currentFileTreeWidth = this.fileTreeWidth; + this.currentTerminalHeight = this.terminalHeight; + if (this.executionEnvironment) { await this.initializeWorkspace(); } @@ -1315,4 +1483,35 @@ testSmartPromise(); resource: { path: m.resource.path }, })); } + + // ========== Public Layout Methods ========== + + /** + * Programmatically set the file tree width + */ + public setFileTreeWidth(width: number): void { + const minWidth = 150; + const maxWidth = this.getBoundingClientRect().width * 0.5; + this.currentFileTreeWidth = Math.max(minWidth, Math.min(maxWidth, width)); + window.dispatchEvent(new Event('resize')); + } + + /** + * Programmatically set the terminal height + */ + public setTerminalHeight(height: number): void { + const minHeight = 100; + const maxHeight = this.getBoundingClientRect().height * 0.7; + this.currentTerminalHeight = Math.max(minHeight, Math.min(maxHeight, height)); + window.dispatchEvent(new Event('resize')); + } + + /** + * Reset layout to initial property values + */ + public resetLayout(): void { + this.currentFileTreeWidth = this.fileTreeWidth; + this.currentTerminalHeight = this.terminalHeight; + window.dispatchEvent(new Event('resize')); + } }