From 7f3a4655958f160eb3a40933b643befdd77cb6c8 Mon Sep 17 00:00:00 2001 From: Thales <> Date: Sun, 30 Aug 2026 17:05:51 +0100 Subject: [PATCH 1/5] Fit the waveform lanes to the panel whenever it changes size The lanes were sized once, when a track loaded, and never again. Everything the panel gained after that became empty space under them rather than taller lanes. Resizing the window was enough to show it. On a 1366x768 window the panel is 370px and each lane 72px; drag the window to 1200px tall and the panel becomes 802px while the lanes stay at 72, so 432px of it goes unused. The bigger the display, the more of it the waveforms refused to occupy. Three things had to change for the lanes to actually follow. _applyLaneHeight runs again on a resize, from an observer on the wave panel. The panel is flex: 1 inside a fixed-height column, so its height comes from its parent and never from the lanes: writing lane heights from that callback cannot feed it its own output. .waves-column takes a floor from the stack height that function already computes. Its natural height is the multitrack's, whose lane height is fixed when the tracks are created, so without a floor the column stayed 432px however much room it was given and the lanes distributed across the old size. And the height maths allows for the separators. It divided the whole panel by the lane count, but the stack is lanes plus the 2px between them, so it overshot by exactly that. Invisible for as long as the lanes sat at their 70px floor and overflowed anyway; it surfaces as a 7px scrollbar the moment they fit. The streaming path keeps its load-time height deliberately. Its lanes are WaveSurfer canvases, and setOptions({height}) only re-renders the ones that have audio, so a lane for a stem the user did not extract keeps the old size: 93, 93, 93, 70, 70, 93 on a six-lane job with two empty, against mixer rows all at 95. A mixer column out of step with its waveforms is worse than unused space, so the refit is limited to the path where the SVG overlay is what you see. Closes #497 --- static/css/daw.css | 9 +++++++-- static/js/player.js | 44 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/static/css/daw.css b/static/css/daw.css index 14dbd481..138ae4e0 100644 --- a/static/css/daw.css +++ b/static/css/daw.css @@ -2045,10 +2045,15 @@ input, textarea { font-family: inherit; } .daw.engine-waveforms .stem-waveform-layer { display: flex !important; } -/* waves-column must size naturally now that multitrack is in flow */ +/* waves-column must size naturally now that multitrack is in flow -- but not + below the stack height _applyLaneHeight computed. Its natural height is the + multitrack's, whose lane height is fixed when the tracks are created, so on + its own the column stays whatever size it was at load however much room the + panel is given. The var is already kept in step with the panel, so a floor is + all that is needed. */ .daw .waves-column { height: auto !important; - min-height: 0 !important; + min-height: var(--wave-widget-track-stack-h, 0) !important; } .loop-region.hidden { display: none !important; } diff --git a/static/js/player.js b/static/js/player.js index b284861a..8c2c322c 100644 --- a/static/js/player.js +++ b/static/js/player.js @@ -976,17 +976,56 @@ export function buildStripStems() { } } +// The lane count the panel is currently laid out for, and the observer that +// re-fits it. _applyLaneHeight divides the wave panel's height between the +// lanes, so it has to run again whenever that height changes -- which it now +// does on demand, because collapsing a panel (#480) hands its height straight +// to this one. Without this the lanes keep the size they were given at load and +// the reclaimed space becomes a gap under them: 141px of it with all three +// panels collapsed. +let _laneCount = 0; +let _laneFitObs = null; + +function _watchLaneFit(count) { + _laneCount = count; + _laneFitObs?.disconnect(); + const panel = document.querySelector(".daw-wave-panel"); + if (!panel) return; + // The panel is flex: 1 inside a fixed-height column, so its own height comes + // from its parent and never from the lanes. Writing lane heights from here + // cannot feed the observer its own output. + _laneFitObs = new ResizeObserver(() => { + // Only where the SVG overlay is the visible waveform. On the streaming path + // the lanes are WaveSurfer canvases sized when the tracks are created, and + // setOptions only re-renders the ones that have audio: a lane for a stem + // the user did not extract keeps its old height and the two columns drift + // apart. Measured on a six-lane job with two empty: 93, 93, 93, 70, 70, 93 + // against mixer rows all at 95. Leaving that path at its load-time height + // costs it the reclaimed space and keeps it aligned, which is the better + // trade for an opt-out path. + if (!document.querySelector(".app")?.classList.contains("engine-waveforms")) return; + if (_laneCount > 0) _applyLaneHeight(_laneCount); + }); + _laneFitObs.observe(panel); +} + function _applyLaneHeight(count) { const wavePanel = document.querySelector(".daw-wave-panel"); const panelH = wavePanel?.clientHeight ?? 0; + // The separators between lanes are part of the stack, so the height available + // to the lanes themselves is the panel minus them. Dividing the whole panel by + // the lane count overshoots by exactly that much, which stayed invisible while + // the lanes sat at their floor and overflowed anyway -- and became a 7px + // scrollbar the moment collapsing a panel let them actually fill it. + const gaps = Math.max(0, count - 1) * WAVEFORM_SEPARATOR_HEIGHT; const laneH = panelH > 0 && count > 0 - ? Math.max(WAVEFORM_LANE_HEIGHT, Math.floor(panelH / count)) + ? Math.max(WAVEFORM_LANE_HEIGHT, Math.floor((panelH - gaps) / count)) : WAVEFORM_LANE_HEIGHT; const appEl = document.querySelector(".app"); appEl?.style.setProperty("--lane-h", `${laneH + 2}px`); appEl?.style.setProperty( "--wave-widget-track-stack-h", - `${count * laneH + (count - 1) * WAVEFORM_SEPARATOR_HEIGHT}px`, + `${count * laneH + gaps}px`, ); return laneH; } @@ -1171,6 +1210,7 @@ export function wireUpAudio(jobId, stems, duration, thumbnail, mixUrl = null, ti } const laneH = _applyLaneHeight(orderedNames.length); + _watchLaneFit(orderedNames.length); const mt = Multitrack.create( orderedNames.map((name, i) => ({ From 282ec6c77b46b618ce215c3b33185ed9eb399fce Mon Sep 17 00:00:00 2001 From: Thales <> Date: Sun, 30 Aug 2026 17:06:15 +0100 Subject: [PATCH 2/5] Let the user put a panel away when they are not using it Six stems separated and two visible: on a 1366x768 laptop the panels around the mixer took roughly two thirds of the window and the mixer got what was left (#480). Making them smaller only moved the number, which is why that issue was left open after the last round trimmed 116px out of them. The reporter had already said what the actual problem was: every one of those panels shows something useful [...] The issue is that they are all mandatory at all times. I do not need the presence percentages while I am setting fader levels, and I do not need the waveform while I am reading the analysis. Right now the app has no way to say that. So this is a way to say it. Three toggles collapse the analysis header, the sections bar and the footer timeline, each handing its height straight to the mixer: default mixer 370px 5 of 6 lanes scrolls analysis off mixer 443px 6 of 6 lanes fits all three off mixer 573px 6 of 6 lanes fits One click on Analysis is enough. Together they are worth 202px. They collapse to nothing rather than to a stub, which is only possible because the controls are somewhere else: a stub tall enough to hold its own control costs most of what collapsing the smaller panels returns. So all three live in one row under the composer, sharing the right edge and the exact width of the two controls above them, from the same --composer-action-w those are sized from. A longer translation widens all three together instead of leaving the row ragged. The topbar does not grow: it already carried 27px of slack around a 50px composer, and the row fits inside it. Legible means the panel is there, struck through means it is put away. The other way round -- highlighting the hidden ones -- inverts what a pressed toggle looks like everywhere else and makes the default state the loud one. A class on .app and a flag in localStorage each, the same shape as the sidebar collapse, with no state anywhere else. This does not decide what the studio shows on first open. Every panel is still there by default, which is a separate question from whether the user can put one away. Closes #480 --- static/css/daw.css | 88 +++++++++++++++++++++++++++++++-- static/index.html | 29 +++++++++++ static/js/i18n.js | 108 +++++++++++++++++++++++++++++++++++++++++ static/js/ui-chrome.js | 46 ++++++++++++++++++ 4 files changed, 267 insertions(+), 4 deletions(-) diff --git a/static/css/daw.css b/static/css/daw.css index 138ae4e0..37695132 100644 --- a/static/css/daw.css +++ b/static/css/daw.css @@ -42,7 +42,12 @@ input, textarea { font-family: inherit; } TOPBAR ═══════════════════════════════════ */ .daw-topbar { - height: 77px; + /* Was a flat 77px. The panel-toggle row under the composer needs a second + line, and auto height means a translation that wraps grows the bar rather + than being clipped. */ + min-height: 77px; + padding-top: 10px; + padding-bottom: 10px; flex-shrink: 0; background: var(--bg-2); border-bottom: 1px solid var(--border); @@ -1687,6 +1692,80 @@ input, textarea { font-family: inherit; } padding: 0 14px; } +/* ── Panel toggles (#480) ── */ +/* A second row under the composer, aligned to its right-hand end so it reads as + belonging to the two controls above it. Collapse to nothing, not to a stub: + the whole point is the height back, and a stub tall enough to hold a control + is most of what the smaller panels are worth. That is only possible because + the controls live up here, where the way back is always in the same place. + + This row costs the topbar about 19px, against up to 202px it can return. */ +.daw-composer-stack { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 3px; +} +.daw-composer-stack > .daw-composer { flex: none; } +/* Exactly as wide as the two controls above it, and sharing their right edge: + the same --composer-action-w they are each sized from, so a longer + translation widens all three together instead of leaving this one ragged. + margin-left:auto does the alignment, since the stack is the composer's width. */ +.daw-panel-toggles { + width: calc(var(--composer-action-w) * 2); + margin-left: auto; + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: space-between; + gap: 2px; + padding: 2px 7px; + background: var(--panel); + border: 1px solid var(--border-strong); + border-radius: 6px; + min-width: 0; +} +.daw-panel-toggles-label, +.daw-panel-toggles-sep { + font-size: 8px; + font-weight: 600; + letter-spacing: 0.03em; + text-transform: uppercase; + color: var(--muted); + line-height: 1; + white-space: nowrap; +} +.daw-panel-toggle { + padding: 2px 3px; + border: 0; + border-radius: 4px; + background: none; + color: var(--fg-2); + font-family: inherit; + font-size: 8.5px; font-weight: 600; letter-spacing: 0.03em; + text-transform: uppercase; + line-height: 1; + cursor: pointer; + white-space: nowrap; + transition: color var(--t-fast), background var(--t-fast), opacity var(--t-fast); +} +.daw-panel-toggle:hover { background: var(--panel-2); } +/* Legible means the panel is there, struck through means it is put away. The + other way round -- highlighting the hidden ones -- inverts what a pressed + toggle normally looks like, and makes the default state the loud one. */ +.daw-panel-toggle[aria-pressed="true"] { color: var(--fg-2); } +.daw-panel-toggle[aria-pressed="false"] { + color: var(--muted); + opacity: 0.6; + text-decoration: line-through; + text-decoration-thickness: 1px; +} + +.app.panel-analysis-off .daw-track-header { display: none; } +.app.panel-sections-off .daw-section-ribbon { display: none; } +.app.panel-timeline-off .footer-wave-region { display: none; } + /* ── Waveform header ── */ .daw-wave-header { display: flex; @@ -2048,9 +2127,10 @@ input, textarea { font-family: inherit; } /* waves-column must size naturally now that multitrack is in flow -- but not below the stack height _applyLaneHeight computed. Its natural height is the multitrack's, whose lane height is fixed when the tracks are created, so on - its own the column stays whatever size it was at load however much room the - panel is given. The var is already kept in step with the panel, so a floor is - all that is needed. */ + its own the column stays the size it was at load. Collapsing a panel (#480) + hands this panel height it would then leave as a gap under the waveforms: + 141px of it with all three collapsed. The var is already kept in step with + the panel, so a floor is all that is needed. */ .daw .waves-column { height: auto !important; min-height: var(--wave-widget-track-stack-h, 0) !important; diff --git a/static/index.html b/static/index.html index 848675a4..7d9f58c8 100644 --- a/static/index.html +++ b/static/index.html @@ -29,6 +29,7 @@
+
@@ -122,6 +123,34 @@
+ +
+ Click to collapse + + + + + +
+
+