feat(workspace): add resizable file tree and terminal panes with draggable handles and public layout APIs
This commit is contained in:
10
changelog.md
10
changelog.md
@@ -1,5 +1,15 @@
|
|||||||
# Changelog
|
# 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)
|
## 2025-12-31 - 3.22.0 - feat(workspace)
|
||||||
add resizable markdown editor/preview split with draggable handle and markdown outlet styling/demo
|
add resizable markdown editor/preview split with draggable handle and markdown outlet styling/demo
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@design.estate/dees-catalog',
|
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.'
|
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -262,6 +262,19 @@ testSmartPromise();
|
|||||||
@state()
|
@state()
|
||||||
accessor initOutput: string[] = [];
|
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)
|
// Keyboard shortcut handler (bound for proper cleanup)
|
||||||
private keydownHandler = (e: KeyboardEvent) => {
|
private keydownHandler = (e: KeyboardEvent) => {
|
||||||
// Cmd+S (Mac) or Ctrl+S (Windows/Linux) - Save
|
// 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 = [
|
public static styles = [
|
||||||
themeDefaultStyles,
|
themeDefaultStyles,
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
@@ -293,67 +371,45 @@ testSmartPromise();
|
|||||||
}
|
}
|
||||||
|
|
||||||
.workspace-container {
|
.workspace-container {
|
||||||
display: grid;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-container.with-filetree.with-terminal {
|
.editor-area {
|
||||||
grid-template-columns: auto 1fr;
|
display: flex;
|
||||||
grid-template-rows: 1fr auto;
|
flex-direction: column;
|
||||||
grid-template-areas:
|
flex: 1;
|
||||||
"filetree editor"
|
min-width: 0;
|
||||||
"filetree terminal";
|
overflow: hidden;
|
||||||
}
|
|
||||||
|
|
||||||
.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";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.filetree-panel {
|
.filetree-panel {
|
||||||
grid-area: filetree;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
border-right: 1px solid ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(0 0% 15%)')};
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: width 0.2s ease;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filetree-panel.collapsed {
|
.filetree-panel.collapsed {
|
||||||
width: 0 !important;
|
width: 0 !important;
|
||||||
border-right: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-panel {
|
.editor-panel {
|
||||||
grid-area: editor;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')};
|
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')};
|
||||||
|
flex: 1;
|
||||||
|
min-width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-panel {
|
.terminal-panel {
|
||||||
grid-area: terminal;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(0 0% 15%)')};
|
border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(0 0% 15%)')};
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: height 0.2s ease;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-panel.collapsed {
|
.terminal-panel.collapsed {
|
||||||
@@ -695,14 +751,96 @@ testSmartPromise();
|
|||||||
right: 0;
|
right: 0;
|
||||||
bottom: 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 {
|
public render(): TemplateResult {
|
||||||
const containerClasses = [
|
const containerClasses = [
|
||||||
'workspace-container',
|
'workspace-container',
|
||||||
this.showFileTree && !this.isFileTreeCollapsed ? 'with-filetree' : '',
|
(this.isDraggingFileTree || this.isDraggingTerminal) ? 'dragging' : '',
|
||||||
this.showTerminal ? 'with-terminal' : '',
|
|
||||||
].filter(Boolean).join(' ');
|
].filter(Boolean).join(' ');
|
||||||
|
|
||||||
if (this.isInitializing) {
|
if (this.isInitializing) {
|
||||||
@@ -720,10 +858,11 @@ testSmartPromise();
|
|||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="${containerClasses}">
|
<div class="${containerClasses}">
|
||||||
|
<!-- Filetree panel (full height) -->
|
||||||
${this.showFileTree ? html`
|
${this.showFileTree ? html`
|
||||||
<div
|
<div
|
||||||
class="filetree-panel ${this.isFileTreeCollapsed ? 'collapsed' : ''}"
|
class="filetree-panel ${this.isFileTreeCollapsed ? 'collapsed' : ''}"
|
||||||
style="width: ${this.isFileTreeCollapsed ? 0 : this.fileTreeWidth}px"
|
style="width: ${this.isFileTreeCollapsed ? 0 : this.currentFileTreeWidth}px"
|
||||||
>
|
>
|
||||||
<dees-workspace-filetree
|
<dees-workspace-filetree
|
||||||
.executionEnvironment=${this.executionEnvironment}
|
.executionEnvironment=${this.executionEnvironment}
|
||||||
@@ -731,8 +870,16 @@ testSmartPromise();
|
|||||||
@file-select=${this.handleFileSelect}
|
@file-select=${this.handleFileSelect}
|
||||||
></dees-workspace-filetree>
|
></dees-workspace-filetree>
|
||||||
</div>
|
</div>
|
||||||
|
${!this.isFileTreeCollapsed ? html`
|
||||||
|
<div
|
||||||
|
class="resize-handle-vertical ${this.isDraggingFileTree ? 'dragging' : ''}"
|
||||||
|
@mousedown=${this.handleFileTreeMouseDown}
|
||||||
|
></div>
|
||||||
|
` : ''}
|
||||||
` : ''}
|
` : ''}
|
||||||
|
|
||||||
|
<!-- Editor + Terminal area -->
|
||||||
|
<div class="editor-area">
|
||||||
<div class="editor-panel">
|
<div class="editor-panel">
|
||||||
<div class="tabs-bar">
|
<div class="tabs-bar">
|
||||||
<div class="tabs-container">
|
<div class="tabs-container">
|
||||||
@@ -770,10 +917,19 @@ testSmartPromise();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Horizontal resize handle for terminal -->
|
||||||
|
${this.showTerminal && !this.isTerminalCollapsed ? html`
|
||||||
|
<div
|
||||||
|
class="resize-handle-horizontal ${this.isDraggingTerminal ? 'dragging' : ''}"
|
||||||
|
@mousedown=${this.handleTerminalMouseDown}
|
||||||
|
></div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<!-- Terminal panel -->
|
||||||
${this.showTerminal ? html`
|
${this.showTerminal ? html`
|
||||||
<div
|
<div
|
||||||
class="terminal-panel ${this.isTerminalCollapsed ? 'collapsed' : ''}"
|
class="terminal-panel ${this.isTerminalCollapsed ? 'collapsed' : ''}"
|
||||||
style="height: ${this.isTerminalCollapsed ? 32 : this.terminalHeight}px"
|
style="height: ${this.isTerminalCollapsed ? 32 : this.currentTerminalHeight}px"
|
||||||
>
|
>
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<div class="panel-tabs">
|
<div class="panel-tabs">
|
||||||
@@ -816,6 +972,7 @@ testSmartPromise();
|
|||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -827,6 +984,13 @@ testSmartPromise();
|
|||||||
async disconnectedCallback() {
|
async disconnectedCallback() {
|
||||||
await super.disconnectedCallback();
|
await super.disconnectedCallback();
|
||||||
document.removeEventListener('keydown', this.keydownHandler);
|
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) {
|
if (this.autoSaveInterval) {
|
||||||
clearInterval(this.autoSaveInterval);
|
clearInterval(this.autoSaveInterval);
|
||||||
this.autoSaveInterval = null;
|
this.autoSaveInterval = null;
|
||||||
@@ -835,6 +999,10 @@ testSmartPromise();
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async firstUpdated() {
|
public async firstUpdated() {
|
||||||
|
// Initialize current sizes from properties
|
||||||
|
this.currentFileTreeWidth = this.fileTreeWidth;
|
||||||
|
this.currentTerminalHeight = this.terminalHeight;
|
||||||
|
|
||||||
if (this.executionEnvironment) {
|
if (this.executionEnvironment) {
|
||||||
await this.initializeWorkspace();
|
await this.initializeWorkspace();
|
||||||
}
|
}
|
||||||
@@ -1315,4 +1483,35 @@ testSmartPromise();
|
|||||||
resource: { path: m.resource.path },
|
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'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user