Skip to content

Rebuild the studio's fixed chrome so the mixer gets the room - #491

Merged
thcp merged 2 commits into
mainfrom
feat/studio-layout-density
Aug 29, 2026
Merged

Rebuild the studio's fixed chrome so the mixer gets the room#491
thcp merged 2 commits into
mainfrom
feat/studio-layout-density

Conversation

@thcp

@thcp thcp commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

Four problems in the studio's fixed chrome, all found while measuring where the
mixer's vertical budget goes.

Closes #487
Closes #488
Closes #489
Closes #490

Measured, at 1366x768

The resolution #480 was reported at, library already collapsed.

Before After
Analysis header 147 px 73 px
Footer 212 px 170 px
Mixer ~200 px 376 px
Stem lanes fully visible 2 5 of 6

#480 stays open. The mixer still scrolls at six stems, and that issue asks
for a way to reduce these panels rather than for them to be smaller once. This
is the cheapest part of its budget won back, not the answer to it.

The analysis header (#487)

Each metadata card was three stacked text lines. The qualifier now sits on the
value's own line, E min Natural Minor, and the presence cards read
VOCAL PRESENCE 66% on one line rather than stacked. Type came down with it,
22 px to 17 px and 28 px to 18 px.

Halving it was a question of how many lines a card is, not how big its text is.
Three stacked lines have a floor: 10 px labels are already at the bottom of what
is readable.

The header's two rows (#488)

Seven metadata cards sat above six presence cards, each unaware of the other, so
their dividers landed at different x all the way across. Both rows are now grids
of the same seven columns. Six presence stems fill seven because Vocals takes
two; once the lead/backing split has run it is replaced by two cards and the row
becomes a plain 7x1. Verified: every bottom edge lands on a top edge, in both
states.

The top row had to become a grid too. Flex and grid round fractional track widths
differently, and as a flex row it was a pixel out of step at most widths.

Those two new cards had nothing to show. split_vocals never touched
stem_presence, so after a split neither half had a figure anywhere. Presence is
a stem's RMS as a percentage of the loudest stem in the job, and that reference
is not stored: only the percentages are. The obvious fix decodes all eight stems
again to place two of them on a scale the other six already sit on.
presence_for_split recovers the reference arithmetically instead:

loudest = rms[vocals] / (presence[vocals] / 100)

merge_stem_peaks already measured RMS for the new stems in its existing pass
and discarded it. It returns it now, with vocals riding along in the same pass:
one extra file scanned. With no reference recorded it returns nothing and the
cards read --, which is honest. Three tests cover it, including the empty case.

The footer's controls (#489)

The clusters sat in a wrapping flex row, so the click track either shared the
line, took its own, or stayed and split its own options across two rows. The
footer was never the same shape twice.

Nothing wraps and nothing shrinks now. Verified at 1920, 1600, 1280 and 1024:
one row, order fixed at transport, position, speed, key, click, every cluster
34 px tall. A window too narrow to hold the row scrolls it sideways, on the same
thin auto-hide scrollbar as the library list and mixer.

The row is top-aligned, so every label sits on one line no matter how tall the
controls under it are. Bottom-aligned, the click track threw its own label a row
above the other four.

That also settled the dividers. They were hidden when stranded at the end of a
wrapped line, but bottom-alignment meant the check never matched and none were
ever drawn. Nothing can be stranded now, so syncFooterDividers and the
is-orphan rule are deleted rather than left as logic that cannot fire.

min-height: 200px dated from when the click cluster could wrap. Its content
needs 170, so the floor only padded the footer past itself and the surplus
pooled as a gap under whichever tier came last.

The track identity (#490)

It was holding a 300 px column of the footer to answer a different question from
everything beside it: the footer's left column said what is loaded, the rest said
how it plays.

It went to the library sidebar first, and came back. The gutter beside the
timeline has to exist regardless, because the timeline's left edge lands on the
lane waveforms' left edge so a position reads at the same x in both. Leaving the
identity in the sidebar meant a panel whose height had to be measured and
mirrored against the footer's, and it put the loaded track's name in the column
you read to choose a different one.

So it is back in the gutter, but in the tier that gutter belongs to rather than
as a full-height column beside the controls. The controls keep the left edge they
gained, and the block is one tier tall instead of two: cover and title on a line
with the favourite, the meta line under them. 79 px, against 141 in the sidebar.

Export Mix moved to the right of the controls row. It acts on the track rather
than on playback, so it sits opposite the clusters instead of filed among them,
and its menu opens toward the middle of the window: anchored left, a 260 px menu
on the footer's right edge would hang off it.

That placement needed the row split. The clusters scroll sideways when the window
cannot hold them, and anything after them would scroll off with them, so
.footer-clusters takes the overflow and the actions sit outside it. Export Mix
is reachable at any width.

Its footer-* classes travelled as track-panel-* and track-chip-* while it
was in the sidebar, and kept those names: they describe the block, not where it
sits. Every id is unchanged, so no JS or test needed touching beyond one selector
in main.js.

Measured: lane panel and footer timeline both at x=690; identity slot 390 to 690;
Export Mix right edge 1582, flush with the timeline's.

The rail reads Favorites, Library, Queue, Trash. Queue was below Trash, an
active state filed under an archive.

Verification

66 browser tests pass, including the whole export-menu suite, which exercises the
export button in its new home. 845 backend tests pass; the 14 failures are
pre-existing and reproduce identically on main, nine of them because this
machine has no ffmpeg. ruff check and ruff format --check clean, node --check clean on every file. i18n complete at 468 keys across all nine tables:
presence.vocalGlobal added, presence.vocal removed.

Before merging

  • uv.lock is untouched, so the desktop in-app updater is unaffected.
  • The Unraid template still pins 0.15.2.

Thales added 2 commits August 29, 2026 02:30
The panels above and below the mixer took roughly two thirds of a 1366x768
window and the mixer got what was left, which was two of six stem lanes (#480).
Working through that turned up four separate problems, all in the same few
files, so they land together.

Analysis header, 147px to 73px (#487). Each metadata card was three stacked text
lines; the qualifier (Natural Minor, Peak 3.5 dB, High) now sits on the value's
own line, and the presence cards put their label and percentage side by side
rather than one over the other. Type came down with it, 22px to 17px and 28px to
18px. Halving it was a question of how many lines a card is, not how big its
text is.

Both header rows are now grids of the same seven columns, so their dividers land
on the same pixels (#488). Six presence stems fill seven columns because Vocals
takes two; once the lead/backing split has run it is replaced by two cards and
the row becomes a plain 7x1. A flex row and a grid round fractional track widths
differently, which is why the top row is a grid now too: it was a pixel out of
step at most window widths.

Those two new cards had nothing to show. split_vocals never touched
stem_presence, so neither half had a figure anywhere. Presence is a stem's RMS
as a percentage of the loudest stem in the job and that reference is not stored,
only the percentages are, so the obvious fix is decoding all eight stems again
to place two of them on a scale the other six already sit on.
presence_for_split recovers the reference arithmetically instead:

    loudest = rms[vocals] / (presence[vocals] / 100)

merge_stem_peaks already measured RMS for the new stems in its existing pass and
threw it away; it returns it now, and vocals rides along in the same pass. One
extra file scanned. With no reference recorded it returns nothing and the cards
read "--", which is honest.

The footer is one line, always (#489). Its clusters sat in a wrapping flex row,
so the click track either shared the line, took its own, or stayed and split its
own options across two rows: the footer was never the same shape twice. Nothing
wraps and nothing shrinks now, and a window too narrow to hold the row scrolls
it sideways. The row is top-aligned too, so every label sits on one line no
matter how tall the controls under it are; bottom-aligned, the click track threw
its own label a row above the other four.

That also settled the dividers. They were hidden when stranded at the end of a
wrapped line, but bottom-alignment meant the check never matched and none were
ever drawn. With no wrapping none can be stranded, so syncFooterDividers and the
is-orphan rule are gone rather than left as logic that cannot fire.

Footer height, 212px to 170px. The min-height dated from when the click cluster
could wrap and a rigid height would have clipped it. Its content needs 170, so
the floor only padded the footer past itself and the surplus pooled as a gap
under whichever tier came last.

The track identity moved to the bottom of the library sidebar (#490), pinned
below the list so it stays put while the folders and Tags scroll. It was holding
a 300px column of the footer to answer a different question from everything
beside it. Its footer-* classes came with it as track-panel-* and track-chip-*:
they no longer live in the footer, and the old names would have misdirected the
next reader. The panel matches the footer's height exactly, measured live by
syncTrackPanelHeight, because the footer's height is not a constant.

The 300px the footer loses stays reserved, because the timeline's left edge has
to land on the lane waveforms' left edge for a position to read at the same x in
both. Export Mix fills that gutter rather than leaving it empty. The gutter's
width is the offset, so the alignment and the button placement are one fact
instead of two numbers that have to agree.

The rail reads Favorites, Library, Queue, Trash. Queue was below Trash, an
active state filed under an archive.

Five of six lanes are visible at 1366x768 now, against two before. The mixer
still scrolls, so #480 stays open.
Moving the identity block to the library sidebar freed the footer's column, but
the 300px gutter beside the timeline still had to exist: the timeline's left
edge lands on the lane waveforms' left edge so a position reads at the same x in
both. Export Mix filled it, which left the identity in a sidebar panel whose
height had to be measured and mirrored, and put the loaded track's name in the
column the user reads to choose a different one.

So the identity is back where it started, in the gutter, but in the tier that
gutter belongs to rather than as a full-height column beside the controls. The
controls keep the left edge they gained, and the block is one tier tall instead
of two: cover and title on a line with the favourite, the meta line under them.
79px against the sidebar panel's 141.

Export Mix moves to the right of the controls row. It acts on the track rather
than on playback, so it sits opposite the clusters instead of filed among them,
and its menu now opens toward the middle of the window: anchored left, a 260px
menu on the footer's right edge would hang off it.

That placement needed the row split. The clusters scroll sideways when the
window cannot hold them, and anything after them would scroll off with them, so
.footer-clusters takes the overflow and the actions sit outside it. Export Mix
stays reachable at any width.

The favourite rides the title line rather than going with Export Mix. It marks
the track, so it belongs with what it marks.

With the sidebar panel gone, syncTrackPanelHeight and --daw-footer-h go too:
nothing needs to mirror the footer's height any more.
@thcp
thcp merged commit 3a52296 into main Aug 29, 2026
10 checks passed
@thcp
thcp deleted the feat/studio-layout-density branch August 29, 2026 14:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment