Reorderable columns, a loaded first file, and a spectrogram worth keeping

Drag a heading and the column moves, with a copy of it following the pointer
and the column it came from stepping back. One array of keys — every column,
shown or hidden — backs the table, the Settings list and what is saved, so a
hidden column keeps its neighbour and returns where it was rather than at the
end. A drag ends in a click, and the click is what sorting listens for, so
that click is swallowed.

Opening a folder loads its first file: the read, the peaks, the channel chips
and the waveform, then paused at the start rather than playing. playRow gained
a paused mode instead of a second loader that could drift from it. It cannot
stop a folder opening, and a file that will not decode no longer writes a
playback error over the line saying the folder opened — nobody asked it to
play. The badge follows the transport now, so a paused file stops claiming to
be playing.

A bulk run re-reads every file it touches, but nothing told the transport to
look again: rename the boom across a card and the mixer went on showing the
old name for the very file it had just rewritten.

Settings loses its Done button and the mixer its Close — both apply as you
touch them, and the window's own close is enough. Save Image moves onto the
file name's line; the rule meant to do that had been dead for some time,
overwritten by the shared modal header. The exported spectrogram carries the
folder, the file and the frequency axis, drawn in, because a PNG read
somewhere else has none of that. The viewer is unchanged.

Mixer track names wear the player's pill, the waveform thumbnails give their
grey panel back to the row, and every button is the transport's size.

Build output that had been committed by accident is untracked, and .gitignore
grows the entries for it.

459 checks.
This commit is contained in:
2026-09-04 00:28:31 +02:00
parent 16c3e6e103
commit 6b7c09bacd
8 changed files with 1163 additions and 34 deletions
+117 -15
View File
@@ -1038,21 +1038,10 @@ html:has(.bwfa-scope[data-bwfa-theme="dark"]) {
transform: translateY(0.15em);
}
/* Save Image sits with the title, on the right, where the modal's own
heading block already reserves the room. */
.bwfa-scope .bwfa-spectro-head {
display: flex;
align-items: flex-end;
gap: var(--space-4);
}
.bwfa-scope .bwfa-spectro-head h4,
.bwfa-scope .bwfa-spectro-head p {
flex: none;
}
/* The layout of this block is further down, after the shared modal header
rule that sets every dialog's heading to display: block — anything about
it written here is overwritten by that. */
.bwfa-scope .bwfa-spectro-save {
margin-left: auto;
flex: none;
}
@@ -1265,6 +1254,36 @@ html:has(.bwfa-scope[data-bwfa-theme="dark"]) {
color: var(--color-text-muted);
}
/* ---- Save Image belongs on the file name's line --------------------
The spectrogram's heading holds three things where the other dialogs
hold two, and the rule above lays every heading out as a plain block —
so the button, being inline, dropped to a line of its own under the
name. Laid out as a wrapping flex row instead: the name and the button
share the first line, the reading takes the second.
This has to sit after that shared rule rather than with the rest of the
spectrogram's styling further up. An earlier attempt lived up there and
was overwritten by it, which is why nothing moved.
------------------------------------------------------------------ */
.bwfa-scope .bwfa-spectro-head {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0 var(--space-4);
}
.bwfa-scope .bwfa-spectro-head h4 {
flex: none;
}
.bwfa-scope .bwfa-spectro-head p {
flex: 1 0 100%;
}
.bwfa-scope .bwfa-spectro-save {
margin-left: auto;
}
/* ---- 3. The body ---- */
.bwfa-scope .modal-body,
.bwfa-scope .bwfa-sheet-dialog .bwfa-export-fields,
@@ -1412,11 +1431,27 @@ html:has(.bwfa-scope[data-bwfa-theme="dark"]) {
min-width: 0;
}
/* A track name reads the same here as it does in the player's row: the same
pill, the same size, the same colours, so the two places that list the
tracks of a file look like the same thing.
It takes the look and not the behaviour — in the player the pill is the
mute button, here it is a label and mute has its own M — so no pointer
cursor and none of the muted/solo states. inline-block rather than the
chip's inline-flex, because the name has to be able to ellipsis inside a
9em column and text-overflow doesn't reach the children of a flex box. */
.bwfa-scope .bwfa-mixer-name {
display: inline-block;
max-width: 100%;
padding: 0.3rem 0.5rem 0.3rem 0.85rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-pill);
background-color: var(--color-surface-alt);
font-size: var(--fs-sm);
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 500;
}
.bwfa-scope .bwfa-mixer-fader {
@@ -1474,6 +1509,73 @@ html:has(.bwfa-scope[data-bwfa-theme="dark"]) {
min-width: 6em;
}
/* ---- Dragging a column heading ---------------------------------------
The heading you are holding, following the pointer, so the gesture says
what it is moving. Everything here is the app's own surface, border and
text — the only new thing is the shadow, which is what lifts it off the
table and tells you it is in your hand rather than in the row.
It exists only while a drag is in progress, and it never takes a click:
the pointer has to reach the headings underneath to know where it is.
------------------------------------------------------------------- */
.bwfa-scope .bwfa-col-ghost {
position: fixed;
z-index: 60;
pointer-events: none;
padding: 0.3rem 0.6rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background: var(--color-surface);
color: var(--color-text);
font-size: var(--fs-sm);
font-weight: var(--fw-semibold);
white-space: nowrap;
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);
}
/* And the column it came from steps back while it is out of place. */
.bwfa-scope .bwfa-table thead th.is-dragging {
opacity: 0.45;
}
/* The waveform thumbnails sat on their own grey panel, which drew a row of
boxes down the table and cut across the striping and the hover. The
drawing itself only ever clears the canvas, so taking the fill off is
enough for the row's own background to show through — the wave keeps its
blue, and the column stops being a column of tiles. */
.bwfa-scope .bwfa-waveform-thumb {
background-color: transparent;
}
/* ---- One button size --------------------------------------------------
The transport's buttons set the standard, and the standard is what the
framework calls .btn-sm. Everything else was a size larger, so a modal's
Close and the toolbar's buttons stood a good 8px taller than the Play
button right beneath them.
Done here, on .btn itself, rather than by adding .btn-sm to every button
in the markup: buttons get built in JavaScript too, and a rule covers the
ones written later without anybody having to remember. .btn-sm keeps
working and is now simply the same thing.
Not included, and deliberately: .bwfa-round-btn, .modal-close and .chip
are their own shapes and carry no .btn at all, so none of this reaches
them.
This sits after the framework's own .btn-sm / .btn-lg / .btn-icon and
matches their specificity, so it outranks all three. .btn-sm is now the
same declaration twice over; .btn-lg and .btn-icon are unused, and a
size modifier would have to be written to beat this rule to work again.
The one button it does not size is the Choose beside the export
destination, which takes its height from the field it stands next to.
--------------------------------------------------------------------- */
.bwfa-scope .btn {
padding: 0.35rem 0.75rem;
font-size: var(--fs-sm);
}
/* The word inside the transport button is not a target. It is rewritten as
playback runs, and a node that changes under the pointer between mousedown
and mouseup takes the click with it. */