feat: add resizable columns and horizontal scrolling

- Columns can be resized by dragging the border between them
- Column widths persist during navigation (150px min, 500px max)
- Container scrolls horizontally when columns exceed available space
- Auto-scrolls to show newly opened columns
- Resize handle highlights on hover/active state
This commit is contained in:
2026-01-23 23:58:19 +00:00
parent 2edec4013f
commit cf07f8cad9
2 changed files with 105 additions and 9 deletions

View File

@@ -8,6 +8,7 @@ interface IColumn {
objects: IS3Object[];
prefixes: string[];
selectedItem: string | null;
width: number;
}
@customElement('tsview-s3-columns')
@@ -24,6 +25,11 @@ export class TsviewS3Columns extends DeesElement {
@state()
private accessor loading: boolean = false;
private resizing: { columnIndex: number; startX: number; startWidth: number } | null = null;
private readonly DEFAULT_COLUMN_WIDTH = 250;
private readonly MIN_COLUMN_WIDTH = 150;
private readonly MAX_COLUMN_WIDTH = 500;
public static styles = [
cssManager.defaultStyles,
css`
@@ -31,25 +37,57 @@ export class TsviewS3Columns extends DeesElement {
display: block;
height: 100%;
overflow-x: auto;
overflow-y: hidden;
}
.columns-container {
display: flex;
height: 100%;
min-width: 100%;
min-width: max-content;
}
.column-wrapper {
display: flex;
height: 100%;
flex-shrink: 0;
}
.column {
min-width: 220px;
max-width: 280px;
border-right: 1px solid #333;
display: flex;
flex-direction: column;
height: 100%;
flex-shrink: 0;
overflow: hidden;
}
.column:last-child {
border-right: none;
.resize-handle {
width: 5px;
height: 100%;
background: transparent;
cursor: col-resize;
position: relative;
flex-shrink: 0;
}
.resize-handle::after {
content: '';
position: absolute;
top: 0;
left: 2px;
width: 1px;
height: 100%;
background: #333;
}
.resize-handle:hover::after,
.resize-handle.active::after {
background: #6366f1;
width: 2px;
left: 1px;
}
.column-wrapper:last-child .resize-handle {
display: none;
}
.column-header {
@@ -151,6 +189,7 @@ export class TsviewS3Columns extends DeesElement {
objects: result.objects,
prefixes: result.prefixes,
selectedItem: null,
width: this.DEFAULT_COLUMN_WIDTH,
},
];
} catch (err) {
@@ -182,8 +221,11 @@ export class TsviewS3Columns extends DeesElement {
objects: result.objects,
prefixes: result.prefixes,
selectedItem: null,
width: this.DEFAULT_COLUMN_WIDTH,
},
];
// Auto-scroll to show the new column
this.updateComplete.then(() => this.scrollToEnd());
} catch (err) {
console.error('Error loading folder:', err);
}
@@ -192,6 +234,48 @@ export class TsviewS3Columns extends DeesElement {
// The navigate event is only for breadcrumb sync, not for column navigation
}
private scrollToEnd() {
this.scrollLeft = this.scrollWidth - this.clientWidth;
}
private startResize(e: MouseEvent, columnIndex: number) {
e.preventDefault();
this.resizing = {
columnIndex,
startX: e.clientX,
startWidth: this.columns[columnIndex].width,
};
document.addEventListener('mousemove', this.handleResize);
document.addEventListener('mouseup', this.stopResize);
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
}
private handleResize = (e: MouseEvent) => {
if (!this.resizing) return;
const delta = e.clientX - this.resizing.startX;
const newWidth = Math.min(
this.MAX_COLUMN_WIDTH,
Math.max(this.MIN_COLUMN_WIDTH, this.resizing.startWidth + delta)
);
this.columns = this.columns.map((col, i) => {
if (i === this.resizing!.columnIndex) {
return { ...col, width: newWidth };
}
return col;
});
};
private stopResize = () => {
this.resizing = null;
document.removeEventListener('mousemove', this.handleResize);
document.removeEventListener('mouseup', this.stopResize);
document.body.style.cursor = '';
document.body.style.userSelect = '';
};
private selectFile(columnIndex: number, key: string) {
// Update selection
this.columns = this.columns.map((col, i) => {
@@ -240,7 +324,19 @@ export class TsviewS3Columns extends DeesElement {
return html`
<div class="columns-container">
${this.columns.map((column, index) => this.renderColumn(column, index))}
${this.columns.map((column, index) => this.renderColumnWrapper(column, index))}
</div>
`;
}
private renderColumnWrapper(column: IColumn, index: number) {
return html`
<div class="column-wrapper">
${this.renderColumn(column, index)}
<div
class="resize-handle ${this.resizing?.columnIndex === index ? 'active' : ''}"
@mousedown=${(e: MouseEvent) => this.startResize(e, index)}
></div>
</div>
`;
}
@@ -251,7 +347,7 @@ export class TsviewS3Columns extends DeesElement {
: this.bucketName;
return html`
<div class="column">
<div class="column" style="width: ${column.width}px">
<div class="column-header" title=${column.prefix || this.bucketName}>
${headerName}
</div>