From e06bcfa7a9c615ad127fd7293b9d8d8e65a0ef2f Mon Sep 17 00:00:00 2001 From: Thales <> Date: Tue, 1 Sep 2026 10:20:23 +0100 Subject: [PATCH 1/2] feat(panels): clear the studio down to the mixer in one press #480 gave each panel around the mixer a toggle, but collapsing all three still left the library holding its column, which is the largest thing on screen that is not the mixer. Getting to a bare mixer meant three presses in the toggles row and a fourth on a control somewhere else, then four more to undo it. "All" sits at the end of the same row and takes the three panels and the sidebar together. It keeps no state of its own: it drives the same apply/persist the individual toggles already use, and reads its own pressed state back off .app. A fourth flag would be a fourth thing to disagree with the other three the moment the library was collapsed from its own button instead. A MutationObserver on that one class attribute is what keeps it honest, so no other handler has to remember it exists. setSidebarCollapsed moves out of wireCatalogToggle's scope and is exported, because the sidebar's state is one class plus one localStorage flag and two writers reproducing that pair is how it drifts. setCatalogView was already the second writer: it wrote the flag by hand and left the collapse button's aria-expanded claiming the sidebar was still shut. It routes through the helper now. panels.all and panels.allTitle are in all ten language tables. ptPT needs no override, the European wording is the same. --- static/index.html | 8 +++++++ static/js/catalog.js | 40 ++++++++++++++++++++++--------- static/js/i18n.js | 40 +++++++++++++++++++++++++++++++ static/js/ui-chrome.js | 53 ++++++++++++++++++++++++++++++++++++++---- 4 files changed, 125 insertions(+), 16 deletions(-) diff --git a/static/index.html b/static/index.html index c79d4064..f84c92a7 100644 --- a/static/index.html +++ b/static/index.html @@ -148,6 +148,14 @@ title="Show or hide the timeline" data-i18n-title="panels.timelineTitle" data-i18n-aria-label="panels.timelineTitle"> Timeline + + + diff --git a/static/js/catalog.js b/static/js/catalog.js index 1ca361de..116a971c 100644 --- a/static/js/catalog.js +++ b/static/js/catalog.js @@ -623,11 +623,10 @@ function moveTrackToTrash(trackId) { function setCatalogView(view) { catalogView = ["trash", "favorites", "queue"].includes(view) ? view : "library"; - const app = document.querySelector(".app"); - if (catalogView !== "library") { - app?.classList.remove("cat-collapsed"); - localStorage.setItem("stemdeck.catalog.collapsed", "0"); - } + // Switching to Trash, Favourites or Queue is a request to look at the + // sidebar, so a collapsed one comes back. Through the shared helper, or the + // collapse button's aria-expanded is left claiming the sidebar is still shut. + if (catalogView !== "library") setSidebarCollapsed(false); render(); } @@ -1986,6 +1985,31 @@ function applyQueueDecorations(snap = getQueueSnapshot()) { // ─── Catalog panel collapse ─── +/** Collapse or restore the library sidebar. + * + * Exported because the "All" panel toggle puts the library away along with + * the three panels around the mixer. The state is one class on .app plus one + * localStorage flag, and a second writer reproducing those by hand is exactly + * the kind of pair that drifts the first time either changes. + */ +export function setSidebarCollapsed(isCollapsed) { + const app = document.querySelector(".app"); + if (!app) return; + app.classList.toggle("cat-collapsed", isCollapsed); + document + .getElementById("sidebarCollapseBtn") + ?.setAttribute("aria-expanded", String(!isCollapsed)); + try { + localStorage.setItem("stemdeck.catalog.collapsed", isCollapsed ? "1" : "0"); + } catch (e) { + console.warn("[catalog] could not persist sidebar state:", e); + } +} + +export function isSidebarCollapsed() { + return !!document.querySelector(".app")?.classList.contains("cat-collapsed"); +} + function wireCatalogToggle() { const toggle = document.getElementById("catalogToggle"); const collapseBtn = document.getElementById("sidebarCollapseBtn"); @@ -1998,12 +2022,6 @@ function wireCatalogToggle() { collapseBtn?.setAttribute("aria-expanded", "false"); } - function setSidebarCollapsed(isCollapsed) { - app.classList.toggle("cat-collapsed", isCollapsed); - collapseBtn?.setAttribute("aria-expanded", String(!isCollapsed)); - localStorage.setItem("stemdeck.catalog.collapsed", isCollapsed ? "1" : "0"); - } - collapseBtn?.addEventListener("click", () => { setSidebarCollapsed(!app.classList.contains("cat-collapsed")); }); diff --git a/static/js/i18n.js b/static/js/i18n.js index 02fac7fc..6aef643f 100644 --- a/static/js/i18n.js +++ b/static/js/i18n.js @@ -346,6 +346,10 @@ const en = { "panels.sectionsTitle": "Show or hide the sections bar", "panels.timelineTitle": "Show or hide the timeline", + + "panels.all": "All", + + "panels.allTitle": "Show or hide every panel and the library", "mixer.hint": "Drag fader · M/S", "stemsPanel.ariaLabel": "Stems", @@ -929,6 +933,10 @@ const pl = { "panels.sectionsTitle": "Pokaż lub ukryj pasek sekcji", "panels.timelineTitle": "Pokaż lub ukryj oś czasu", + + "panels.all": "Wszystko", + + "panels.allTitle": "Pokaż lub ukryj wszystkie panele i bibliotekę", "mixer.hint": "Przeciągnij suwak · M/S", "stemsPanel.ariaLabel": "Ścieżki", @@ -1502,6 +1510,10 @@ const ja = { "panels.sectionsTitle": "セクションバーの表示を切り替えます", "panels.timelineTitle": "タイムラインの表示を切り替えます", + + "panels.all": "すべて", + + "panels.allTitle": "すべてのパネルとライブラリの表示を切り替えます", "mixer.hint": "フェーダーをドラッグ · M/S", "stemsPanel.ariaLabel": "パート", @@ -2050,6 +2062,10 @@ const zhHans = { "panels.sectionsTitle": "显示或隐藏段落栏", "panels.timelineTitle": "显示或隐藏时间轴", + + "panels.all": "全部", + + "panels.allTitle": "显示或隐藏所有面板和音乐库", "mixer.hint": "拖动推子 · M/S", "stemsPanel.ariaLabel": "音轨", @@ -2598,6 +2614,10 @@ const de = { "panels.sectionsTitle": "Abschnittsleiste ein- oder ausblenden", "panels.timelineTitle": "Zeitleiste ein- oder ausblenden", + + "panels.all": "Alle", + + "panels.allTitle": "Alle Bereiche und die Bibliothek ein- oder ausblenden", "mixer.hint": "Fader ziehen · M/S", "stemsPanel.ariaLabel": "Stems", @@ -3157,6 +3177,10 @@ const pt = { "panels.sectionsTitle": "Mostrar ou ocultar a barra de seções", "panels.timelineTitle": "Mostrar ou ocultar a linha do tempo", + + "panels.all": "Tudo", + + "panels.allTitle": "Mostrar ou ocultar todos os painéis e a biblioteca", "mixer.hint": "Arraste o fader · M/S", "stemsPanel.ariaLabel": "Stems", @@ -3718,6 +3742,10 @@ const id = { "panels.sectionsTitle": "Tampilkan atau sembunyikan bilah bagian", "panels.timelineTitle": "Tampilkan atau sembunyikan lini masa", + + "panels.all": "Semua", + + "panels.allTitle": "Tampilkan atau sembunyikan semua panel dan pustaka", "mixer.hint": "Seret fader · M/S", "stemsPanel.ariaLabel": "Stem", @@ -4266,6 +4294,10 @@ const fr = { "panels.sectionsTitle": "Afficher ou masquer la barre de sections", "panels.timelineTitle": "Afficher ou masquer la chronologie", + + "panels.all": "Tout", + + "panels.allTitle": "Afficher ou masquer tous les panneaux et la bibliothèque", "mixer.hint": "Glissez le fader · M/S", "stemsPanel.ariaLabel": "Pistes", @@ -4936,6 +4968,10 @@ const es = { "panels.sectionsTitle": "Mostrar u ocultar la barra de secciones", "panels.timelineTitle": "Mostrar u ocultar la línea de tiempo", + + "panels.all": "Todo", + + "panels.allTitle": "Mostrar u ocultar todos los paneles y la biblioteca", "mixer.hint": "Arrastra el fader · M/S", "stemsPanel.ariaLabel": "Stems", @@ -5518,6 +5554,10 @@ const ko = { "panels.sectionsTitle": "구간 막대 표시하거나 숨기기", "panels.timelineTitle": "타임라인 표시하거나 숨기기", + + "panels.all": "전체", + + "panels.allTitle": "모든 패널과 라이브러리 표시하거나 숨기기", "mixer.hint": "페이더 드래그 · M/S", "stemsPanel.ariaLabel": "스템", diff --git a/static/js/ui-chrome.js b/static/js/ui-chrome.js index bfc3b671..8eeb8ab6 100644 --- a/static/js/ui-chrome.js +++ b/static/js/ui-chrome.js @@ -2,6 +2,8 @@ // attributes so the Content-Security-Policy can forbid inline script (#171). // Loaded as a module (deferred), so the DOM is parsed before this runs. +import { setSidebarCollapsed, isSidebarCollapsed } from "./catalog.js"; + // Upload button → trigger the hidden file input. document.getElementById("uploadFileBtn")?.addEventListener("click", () => { document.getElementById("fileInput")?.click(); @@ -53,6 +55,14 @@ function wirePanelToggles() { } }; + const persist = (name, shown) => { + try { + localStorage.setItem(PANEL_STORE_PREFIX + name, shown ? "1" : "0"); + } catch (e) { + console.warn("[panels] could not persist state:", e); + } + }; + for (const btn of toggles) { const name = btn.dataset.panel; let shown = true; @@ -65,13 +75,46 @@ function wirePanelToggles() { btn.addEventListener("click", () => { const next = app.classList.contains(`panel-${name}-off`); apply(name, next); - try { - localStorage.setItem(PANEL_STORE_PREFIX + name, next ? "1" : "0"); - } catch (e) { - console.warn("[panels] could not persist state:", e); - } + persist(name, next); }); } + + wireAllToggle(app, toggles.map((btn) => btn.dataset.panel), apply, persist); +} + +// "All" clears the studio down to the mixer in one press, and brings it back +// in one more. Clearing the three panels while the library still holds its +// column barely changes what you see, which is what made a fourth button +// worth having rather than three presses. +// +// It keeps no state of its own: it drives the same apply/persist the +// individual toggles use, and reads its own pressed state back off .app. A +// fourth flag would be a fourth thing to disagree with the other three the +// moment the library was collapsed from its own button instead. +function wireAllToggle(app, names, apply, persist) { + const btn = document.querySelector(".daw-panel-toggle[data-panel-all]"); + if (!btn) return; + + const everythingShown = () => + names.every((name) => !app.classList.contains(`panel-${name}-off`)) && !isSidebarCollapsed(); + + const sync = () => btn.setAttribute("aria-pressed", String(everythingShown())); + + btn.addEventListener("click", () => { + // Anything hidden means the press is asking for everything back. + const show = !everythingShown(); + for (const name of names) { + apply(name, show); + persist(name, show); + } + setSidebarCollapsed(!show); + }); + + // The panels and the sidebar can each be moved from their own controls, and + // both land as a class on .app, so watching that one attribute keeps this + // button honest without every other handler having to remember it exists. + new MutationObserver(sync).observe(app, { attributes: true, attributeFilter: ["class"] }); + sync(); } wirePanelToggles(); From a9e13d2e00640003b2ee864e5c33d290cde306e2 Mon Sep 17 00:00:00 2001 From: Thales <> Date: Tue, 1 Sep 2026 11:09:50 +0100 Subject: [PATCH 2/2] feat(footer): scroll a loop bound, and stop the panel row shouting Three things in the footer, all of them small and all of them in the same three files. A loop bound could only be changed by typing it. A loop is never right first time: you drag a rough region, play it, and want the start forty milliseconds earlier because it clips the transient. That meant clicking in and retyping nine characters, which is slower and less precise than dragging the region again, so the fields went unused for the one job they are best at. The wheel now adjusts them, in seconds left of the decimal point and milliseconds right of it, a tenth and a hundredth of a second per notch. One millisecond per notch would need a hundred notches to cover something audible. Character-level hit-testing inside an input is not reliable across browsers, but only the decimal point matters, so the text up to it is measured and compared. A nudge past either end of the track, or one that would squeeze the loop under the minimum, is dropped rather than clamped, so holding the wheel at the end does not drag the other bound along. The panel row read "Click to collapse" as a sentence it had no width for, and All was fused to it. The label is one word now, the rule sits after it, and All is the first of four buttons. Its pressed state greys and strikes it through like the other three, but only once all four are away: read as "is anything hidden" it struck itself through the moment Analysis was hidden alone, which looks exactly like you pressed it. Loop stands beside the two bounds rather than over them. As a row of its own it stretched the footer to three lines for one button. The Alpha badge is gone from Click track. The feature has been through several releases and is covered by its own spec; a warning that has stopped being true also teaches people to ignore the next one. Removed properly: markup, the CSS rule that had no other user, and the dead click.alpha key in all ten tables. --- static/css/daw.css | 57 ++++++++++++++++++++++------- static/index.html | 50 +++++++++++++++++--------- static/js/i18n.js | 81 +++++++++++++++++++++--------------------- static/js/transport.js | 68 +++++++++++++++++++++++++++++++++++ static/js/ui-chrome.js | 27 ++++++++------ 5 files changed, 203 insertions(+), 80 deletions(-) diff --git a/static/css/daw.css b/static/css/daw.css index 70d694e7..e51b2e57 100644 --- a/static/css/daw.css +++ b/static/css/daw.css @@ -1726,6 +1726,14 @@ input, textarea { font-family: inherit; } border-radius: 6px; min-width: 0; } +/* The rule separates the row's label from its controls. It sits after the + label, so "All" reads as the first of four buttons rather than as part of + the words describing them. */ +.daw-panel-toggles-label { + margin-right: 2px; + padding-right: 6px; + border-right: 1px solid var(--border-strong); +} .daw-panel-toggles-label, .daw-panel-toggles-sep { font-size: 8px; @@ -2383,13 +2391,6 @@ input, textarea { font-family: inherit; } display: flex; align-items: center; flex-wrap: nowrap; gap: 8px; min-height: 34px; } -.alpha-badge { - padding: 1px 5px; border-radius: 4px; - background: rgba(74,140,255,0.16); border: 1px solid rgba(74,140,255,0.4); - color: #4a8cff; font-size: 8.5px; font-weight: 700; letter-spacing: 0.05em; - text-transform: uppercase; -} - /* Tier 3: the timeline. Full-bleed rather than an inset rounded panel -- its left edge has to land exactly on the lane waveforms' left edge, and a side border would offset the canvas by its own width. Top and bottom rules only, @@ -2513,7 +2514,18 @@ input, textarea { font-family: inherit; } /* Position group: elapsed / total time, then the loop controls that act on that position (design 1b groups them together, not with the transport). */ -.footer-group-position .footer-group-body { gap: 4px; } +/* The loop column is three rows tall while the rest of this group is one, so + centring it left the two bounds hanging below the elapsed/total readout they + belong to. Aligning to the bottom puts the fields on the readout's line and + lets the button and its hint stack up out of the way. */ +.footer-group-position .footer-group-body { gap: 4px; align-items: flex-end; } +.footer-group-position .footer-elapsed, +.footer-group-position .footer-total, +.footer-group-position .footer-time-sep { + /* Match the fields' box height so the baselines land together, rather than + the text bottom meeting the border bottom. */ + line-height: 22px; +} .footer-elapsed { font-size: 18px; font-weight: 500; letter-spacing: -0.02em; color: var(--fg); } @@ -2523,9 +2535,22 @@ input, textarea { font-family: inherit; } .footer-time-sep { font-size: 12px; color: var(--muted-2); } /* Exact loop start/end inputs (inline, right of the loop button) */ +/* The hint stacks over the two fields rather than beside them: the footer row + is already full, and a note about the fields belongs with the fields. Sized + to sit inside the width the two boxes already take, so it does not widen the + Position group. */ +.footer-loop-times-wrap { + display: flex; flex-direction: column; align-items: stretch; gap: 2px; + margin-left: 6px; +} +.footer-loop-hint { + font-size: 7.5px; font-weight: 600; letter-spacing: 0.02em; + text-transform: uppercase; color: var(--muted); + line-height: 1; white-space: nowrap; text-align: center; user-select: none; +} .footer-loop-times { - display: flex; align-items: center; gap: 5px; - margin-left: 4px; cursor: default; user-select: none; + display: flex; align-items: center; justify-content: center; gap: 5px; + cursor: default; user-select: none; } .loop-times-sep { font-size: 12px; color: var(--muted); flex-shrink: 0; } .loop-time-input { @@ -2536,7 +2561,8 @@ input, textarea { font-family: inherit; } color: var(--fg); font-family: inherit; font-size: 12px; font-weight: 600; font-variant-numeric: tabular-nums; text-align: center; } -.loop-time-input:focus { border-color: var(--accent); } +.loop-time-input { cursor: ns-resize; } +.loop-time-input:focus { cursor: text; border-color: var(--accent); } .loop-time-input:disabled { opacity: 0.45; cursor: not-allowed; } /* Transport group: stop square + a play key wide enough to be the obvious @@ -2572,7 +2598,12 @@ input, textarea { font-family: inherit; } /* Loop sits in the Position group and reads as a modifier on the readout next to it, so it is a short labelled pill rather than a full-height control. */ .footer-group-position .daw-iconbtn.btn-transport.loop { - width: auto; height: 26px; padding: 0 9px; gap: 6px; margin-left: 6px; + /* A tall narrow pill beside the fields, not a bar over them. align-self + overrides the group's flex-end so it fills the two rows the hint and the + bounds take, which is what keeps the footer one line high. */ + flex-direction: column; justify-content: center; + width: 46px; height: auto; align-self: stretch; + padding: 3px 4px; gap: 1px; margin-left: 4px; border-radius: 7px; background: var(--panel-2); border: 1px solid var(--border-strong); color: var(--fg-2); @@ -2581,7 +2612,7 @@ input, textarea { font-family: inherit; } .footer-group-position .btn-transport.loop:hover { background: var(--panel-3); color: var(--fg); } -.loop-btn-label { font-size: 11px; font-weight: 500; white-space: nowrap; } +.loop-btn-label { font-size: 9.5px; font-weight: 500; white-space: nowrap; line-height: 1; } .footer-group-position .btn-transport.loop.active, .footer-group-position .btn-transport.loop.active:hover { background: rgba(74,140,255,0.16); diff --git a/static/index.html b/static/index.html index f84c92a7..f3d29ad5 100644 --- a/static/index.html +++ b/static/index.html @@ -133,7 +133,23 @@ control on each panel would need that panel to keep a stub, and the stub costs most of what collapsing the smaller ones returns. -->