Repository navigation
Make the vocals control honest, keep the Extract row on one line, and link the logo - #679
Merged
Merged
Conversation
added 9 commits
September 23, 2026 08:38
Two buttons sat a few pixels apart in the Extract row, both labelled All, meaning different things. One is every stem. The other is the vocals kept as one track rather than divided into Lead + Backing. The vocal toggle appears whenever Vocals is selected, which is the default, so both were on screen together in the state most people open the app in. The reporter of #657 read them as a duplicate control and asked why there were two (#659). The stem row's All is the older and more general meaning, so the vocal one is the one that changed. Combined reads as the opposite of Lead + Backing without borrowing a word the row already uses, and it carries no second meaning in audio the way Mixed or Single would. Only the value changed, in all ten tables. Nothing keys on the label: the buttons are found by data-mode and class, so this is a rename on screen and nowhere else. The phone UI carries the same toggle with its own hardcoded string, renamed to match, and the three comments that named the pair by its labels now name it correctly. The guard is a unit test rather than an e2e one, because the risk is not the English string. Before this, extract.all and vocalMode.all were identical in every one of the ten tables. A translator reaching for the local word for "all" twice restores the collision in one language and is invisible to anyone who does not read it, so the test asserts every control in that row reads differently, in each language, resolved through the same fallback chain the app uses.
The click-track hint reserves its line with a non-breaking space on an empty ::before. The escape for it was written as a literal NUL byte followed by "a0" rather than as "\00a0", so the stylesheet has carried a control character since the top bar was reworked. CSS preprocessing replaces a NUL with U+FFFD, so the declaration has been rendering a replacement character where the space should be. Nothing looked wrong because the element is visibility: hidden and any content reserves the line equally well, which is also why it survived review. It is not only cosmetic. A NUL makes every text tool treat the file as binary: grep reports "binary file matches" and prints nothing, and git renders no diff for it without --text. That is a poor thing to leave in the largest stylesheet in the repo.
The top left was the wordmark, a separator, and a hidden version span, sized to nothing in particular. It is now the waveform symbol alone, in a cell exactly as wide as the rail below it and closed by the same border, so the bar proper begins where the library does. The width is a variable rather than a number in four places. The rail, the collapsed sidebar and this cell were all 66px written out separately, which is three chances for them to stop agreeing. The symbol is not centred on its own artboard: the bars run x110 to x452 of a 512 box, so their middle sits 25 units right of the middle of the image. Centring the image would push the bars off the rail's centre line, which is what it looked like. The 4px offset puts them back on it. The logo is also a link to stemdeck.app, and an anchor rather than a click handler on purpose. main.js already intercepts a[target="_blank"] and hands the URL to the shell through Tauri's open_url; a handler here would have had to repeat that and would have got the desktop case wrong. There is no second tab inside the app's webview, so a link left to itself navigates the window away from StemDeck and strands the user in a web page with no way back. The test for that is the one a browser-only run would pass while the packaged app broke. The tooltip is a translated key, not an English string. It is the only label the logo has, so hardcoding it would make it the one control in the bar that ignores the language setting.
The chips fold into a three dots button as the window narrows, so the bar never gets taller. That was the intent; the row went to two lines anyway, in French and Polish at 1280, on a stock laptop. English fit, which is why nobody saw it. Three separate things were wrong, and each on its own was enough. The row declares flex-wrap: nowrap for itself. A leftover rule from the earlier composer layout set flex-wrap: wrap one class deeper, so the more specific selector won and the row never stopped wrapping. That rule was right when it was written, before folding existed to absorb the overflow. It is gone, and the comment above it no longer describes a row that wraps. The fold measured the row before revealing the button, so it kept one chip too many every time it folded at all. The button takes its width out of the row, not out of the bar, so the chip that had just been measured as fitting no longer did. The button is now shown before the second measurement. Margins were not counted. The Vocals group carries a right margin to set it apart from Drums, and a bounding rect does not include it, so the sum came up short by exactly that margin and the last chip sat a pixel past the edge with the fitter reporting that everything fitted. And the answer could invalidate itself. The row sits in a grid track sized from its own content, so folding a chip shrinks the column and can leave the chips that stayed overflowing a row that genuinely had room for them a moment before. Chinese at 1024 is where that showed. One pass cannot see it, so it now folds, looks again, and folds again until the row is honest. None of this had a test, which is how all three shipped together. There is one now, and it runs in ten languages at four widths, because English at a wide window is the state that proves nothing.
Vocals is the one stem with something to say about how it comes out, so the row has three states for it where every other chip has two: as one track, split into lead and backing, or not extracted. The two mode buttons carry all three. Neither lit is what "no vocals" looks like, and the chip agrees with them. Before this the pair was a consequence of the chip rather than a control. It was hidden until Vocals was pressed, which made the group change shape every time that chip was pressed, and made saying how you want the vocals split something you could only do after asking for them. Dimming it instead of hiding it fixed the shape and left the ordering exactly as wrong. So: pressing the mode already in force switches vocals off, pressing the other switches mode and leaves them on, and pressing either from cold switches them on in that mode. The chip is still a way in and the shorter one, switching vocals on as Combined without a second press. The mode is remembered while the vocals are off, so the buttons return to the way they were last asked for; the chip does not restore it, because that press does not name a mode. Two things came out of the same work. Pressing the last remaining chip used to turn all six stems back on. The set emptied and a guard caught it and refilled it, so narrowing down to one stem and pressing it again produced the most there is, which is the opposite of what the press asked for. An empty row does not need preventing here: All reaches it in one press and is the control whose job that is. And an empty row could be submitted, which was worse than it looked. The server reads an empty stems list as every stem (app/api/jobs.py), so a submit in that state would have extracted the six stems the row was showing as off. Split is disabled while nothing is selected rather than the selection being quietly rewritten underneath. Painting stays in one place. The mode buttons used to set their own aria-pressed from the click handler, which is the shape that put All a state behind its own chips in #658: the painter reachable by a click is never the one that is wrong.
The Extract screen had the arrangement the studio has just moved away from: the Combined / Lead + Backing pair was not rendered at all until the Vocals chip was on, pressing a mode did not select vocals, and pressing the lit one did not switch them off. Leaving it there would be worse than either behaviour on its own. The same control would answer the same question differently depending on which screen it was on, and the phone is the half nobody checks by hand. So the pair is always on screen, a mode is lit only while vocals are actually selected, and the three presses behave as they do in the studio: the mode in force switches vocals off, the other switches mode and leaves them on, either one from cold switches them on in that mode. The chip switches them on as Combined, because that press does not name a mode. The empty-selection trap is here too, and it is the same trap: the server reads an empty stems list as every stem, so the button would have extracted six stems the screen was showing as off. It is disabled instead. This screen had no test but the transpose file, which is how it sat a design behind without anything noticing.
The composer is a form, and a button inside a form with no explicit type is a submit button. The heart has never had one. It was harmless for as long as it lived in the footer, outside the form; the top bar rework moved the now-playing block up into the composer and put the heart inside it without touching the button itself, which is why the diff that broke this does not mention it. So pressing the heart favourited the track and started a split. The one-line fix is type="button". The guard is the part worth having: the composer is where controls keep being added, and the failure is silent because the button does its own job perfectly well while also doing this one. The test asserts that no button in the form is left to default, and that the only submit in it is the one that splits stems, so it covers the next control added to that bar rather than only this one. Removing the attribute again fails three of the four tests, including the one that watches for the submit event.
Two regressions from closing Split on an empty selection, found reviewing the branch before opening it. The studio's Split button has two reasons to be disabled and they were overwriting each other. setSubmitProcessing holds it shut while a submit is in flight, which for a large upload is the whole upload, and the Extract row repaints the same button on every chip press. Pressing a chip during an upload reopened it and allowed a second submit. In the other direction, the end of a submit reopened it unconditionally, so a row emptied during the upload came back with Split live and an empty stems list, which the server reads as every stem. Each side now applies both conditions. On the phone, the empty-selection case was already handled: startExtraction refuses it with a toast saying "Pick at least one stem." The disabled attribute added in 789938e made that worse, not safer. The CTA has no disabled style, so it looked exactly as live as before, and the press was swallowed with no reason given. That commit's message says the trap existed on the phone too; it did not. The attribute is gone and the test now asserts the toast and that nothing is posted.
A track imported from a file has no thumbnail, so its library row and the now-playing square both showed the same note icon, and a library of uploads was a column of identical rows. The extension is the one thing about the file the row can say, so it says that: WAV, FLAC, MP3. This was meant to ship in #665 and half of it did. The styling went out, .thumb-ext and .np-art-placeholder[data-ext], with a comment pairing it with extLabel() in catalog.js. extLabel() was in no commit on any branch: it was lost when that work was rebuilt into commits, and the CSS has styled nothing since. The extension comes from sourceUrl, which is "local:<filename>" for an upload and is the field deriveQuality already reads. It is validated rather than just uppercased, because it goes into markup: no dot, a leading dot, or anything that is not one to five letters and digits falls back to the note. Artwork wins when there is any, and the now-playing square is cleared when the studio empties so it does not keep the last track's extension.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #659
Closes #673
Closes #674
Closes #675
Closes #676
Closes #677
Closes #678
The vocals control (#659, #675)
The vocal mode button is called Combined now, not All. It sat a few pixels from the stem row's All and meant something different. The rename covers all ten language tables. A unit test checks that no two controls in the Extract row share a label in any of them, because before this every table used the same word twice.
Combined and Lead + Backing are now the vocals control, not a setting that waits for the chip. Between them they cover three states: one lit, the other lit, or neither lit, which means no vocals.
The phone's Extract screen now behaves the same way.
Two bugs came out of this:
The Extract row stays on one line (#673)
The row wrapped to a second line at 1280 in French and Polish. English fit, which is why it went unnoticed. There were three causes:
flex-wrap: wraprule was more specific than thenowrapthe row declares, so it won.Folding one chip can also shrink the space for the chips that remain, so it now folds, measures again, and repeats until the row fits. None of this had a test. The new spec runs in ten languages at four widths and checks that the resize watcher does not keep firing.
Smaller fixes
type, so it acted as a submit button. It moved into the form with the now-playing block, which is why the change that broke it never touched the heart. The new test checks every button in the form, so it will catch the next one too.daw.cssmade grep and git treat the file as binary. It is the correct escape now.The logo also sits in a cell exactly as wide as the rail below it, with the rail width now defined in one place.
Verification
uv.lockuntouched.