diff --git a/frontend/index.css b/frontend/index.css index 479f2ef..3df7059 100644 --- a/frontend/index.css +++ b/frontend/index.css @@ -112,8 +112,15 @@ body div.sidebar { } } -.main-panel { +.content-area { grid-area: main-panel; + display: flex; + overflow: hidden; +} + +.main-panel { + flex: 1; + min-width: 0; padding: 0.25em; background-color: #212529; overflow: hidden; diff --git a/frontend/index.html b/frontend/index.html index fed5768..74769cf 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -22,9 +22,12 @@ -
- -
+
+
+ +
+ +
- diff --git a/frontend/src/components/queue-panel/queue-panel.ts b/frontend/src/components/queue-panel/queue-panel.ts index 55dd489..e2472df 100644 --- a/frontend/src/components/queue-panel/queue-panel.ts +++ b/frontend/src/components/queue-panel/queue-panel.ts @@ -1,8 +1,12 @@ -import { LitElement, html, css } from 'lit'; -import { customElement, property } from 'lit/decorators.js'; +import { LitElement, html, css, unsafeCSS } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; import '@awesome.me/webawesome/dist/components/icon/icon.js'; import { QueueController } from '@store/controllers/queue-controller'; +const MIN_WIDTH = 200; +const MAX_WIDTH = 500; +const DEFAULT_WIDTH = 320; + @customElement('queue-panel') export class QueuePanel extends LitElement { private queue = new QueueController(this); @@ -10,26 +14,50 @@ export class QueuePanel extends LitElement { @property({ type: Boolean, reflect: true }) open = false; + @state() + private isDragging = false; + + private panelWidth = DEFAULT_WIDTH; + static override styles = css` :host { - display: block; - position: fixed; - top: 4em; /* below header */ - right: 0; - bottom: 4em; /* above footer */ - width: 320px; - background-color: #1a1a2e; - border-left: 1px solid #333; - transform: translateX(100%); - transition: transform 0.25s ease-in-out; - z-index: 100; + flex-shrink: 0; + width: 0; overflow: hidden; + background-color: #1a1a2e; + transition: width 0.25s ease-in-out; display: flex; - flex-direction: column; + flex-direction: row; } :host([open]) { - transform: translateX(0); + width: var(--queue-width, ${unsafeCSS(DEFAULT_WIDTH)}px); + border-left: 1px solid #333; + } + + .resize-handle { + position: absolute; + top: 0; + left: 0; + width: 4px; + height: 100%; + cursor: col-resize; + background-color: transparent; + transition: background-color 0.15s ease; + z-index: 10; + } + + .resize-handle:hover, + .resize-handle.dragging { + background-color: #6c757d; + } + + .panel-content { + position: relative; + display: flex; + flex-direction: column; + min-width: ${unsafeCSS(MIN_WIDTH)}px; + flex: 1; } .header { @@ -160,9 +188,27 @@ export class QueuePanel extends LitElement { } `; + override connectedCallback() { + super.connectedCallback(); + this.style.setProperty('--queue-width', `${this.panelWidth}px`); + document.addEventListener('mousemove', this.handleMouseMove); + document.addEventListener('mouseup', this.handleMouseUp); + } + + override disconnectedCallback() { + super.disconnectedCallback(); + document.removeEventListener('mousemove', this.handleMouseMove); + document.removeEventListener('mouseup', this.handleMouseUp); + } + private handleClose() { this.open = false; - this.dispatchEvent(new CustomEvent('queue-panel-close', { bubbles: true, composed: true })); + this.dispatchEvent( + new CustomEvent('queue-panel-close', { + bubbles: true, + composed: true, + }), + ); } private handleRemoveTrack(e: Event, position: number) { @@ -174,7 +220,9 @@ export class QueuePanel extends LitElement { this.queue.playAtIndex(index); } - private getDisplayTitle(track: { title: string; filePath: string }): string { + private getDisplayTitle( + track: { title: string; filePath: string }, + ): string { if (track.title) return track.title; // Fall back to filename without extension. @@ -184,49 +232,97 @@ export class QueuePanel extends LitElement { return filename.replace(/\.[^.]+$/, ''); } + private handleMouseDown = (e: MouseEvent) => { + e.preventDefault(); + this.isDragging = true; + + // Disable transition during drag for instant feedback. + this.style.transition = 'none'; + }; + + private handleMouseMove = (e: MouseEvent) => { + if (!this.isDragging) return; + + const rect = this.getBoundingClientRect(); + const newWidth = rect.right - e.clientX; + const clampedWidth = Math.min( + Math.max(newWidth, MIN_WIDTH), + MAX_WIDTH, + ); + + this.panelWidth = clampedWidth; + this.style.setProperty('--queue-width', `${clampedWidth}px`); + }; + + private handleMouseUp = () => { + if (!this.isDragging) return; + + this.isDragging = false; + + // Re-enable transition after drag ends. + this.style.removeProperty('transition'); + }; + override render() { const tracks = this.queue.tracks; const currentIndex = this.queue.currentIndex; return html` -
-

Queue

- -
+
+
+
+

Queue

+ +
- ${tracks.length === 0 - ? html` -
- -

Queue is empty

-

Click a track to start playing

-
- ` - : html` - - `} + ${index + 1} +
+ + ${this.getDisplayTitle(track)} + + + ${track.artist || 'Unknown Artist'} + +
+ + + `, + )} + + `} +
`; } } diff --git a/frontend/src/components/track-list/track-list.ts b/frontend/src/components/track-list/track-list.ts index 162ba9b..3031d82 100644 --- a/frontend/src/components/track-list/track-list.ts +++ b/frontend/src/components/track-list/track-list.ts @@ -62,6 +62,10 @@ export class TrackList extends LitElement { width: 100%; } + .track-row > * { + min-width: 0; + } + .track-row:hover { background-color: rgba(255, 255, 255, 0.05); }