[fix](trx-frontend-http): keep layout sections togglable #24

Merged
sjg merged 1 commits from fix/togglable-layout-sections into main 2026-08-02 18:04:48 +02:00
3 changed files with 74 additions and 12 deletions
@@ -637,6 +637,22 @@ function elementById(id) {
};
const layoutCapabilities = { broadcast: false, digital: false };
let activeRigId = null;
const layoutSections = [
{ id: "advanced-radio-controls", key: "advanced" },
{ id: "audio-controls", key: "audio" },
{ id: "scheduler-controls", key: "scheduler" }
];
const seededSections = /* @__PURE__ */ new Set();
let appliedLayoutName = null;
function seedLayoutSections(layout, layoutChanged) {
layoutSections.forEach(({ id, key }) => {
const section = document.getElementById(id);
if (!section) return;
if (!layoutChanged && seededSections.has(id)) return;
seededSections.add(id);
section.open = layout[key];
});
}
function layoutStorageKey() {
return activeRigId ? `trxOperatorLayout:${activeRigId}` : "trxOperatorLayout";
}
@@ -690,12 +706,9 @@ function elementById(id) {
const layout = layouts[permittedName];
document.body.dataset.operatorLayout = permittedName in layouts ? permittedName : "compact";
if (options.persist !== false) localStorage.setItem(layoutStorageKey(), permittedName);
const details = document.getElementById("advanced-radio-controls");
if (details) details.open = layout.advanced;
const audioDetails = document.getElementById("audio-controls");
if (audioDetails) audioDetails.open = layout.audio;
const schedulerDetails = document.getElementById("scheduler-controls");
if (schedulerDetails) schedulerDetails.open = layout.scheduler;
const layoutChanged = appliedLayoutName !== permittedName;
appliedLayoutName = permittedName;
seedLayoutSections(layout, layoutChanged);
if (options.navigate && typeof browserWindow.navigateToTab === "function") {
browserWindow.navigateToTab(layout.preferredTab);
}
@@ -195,6 +195,31 @@ function elementById<T extends HTMLElement>(id: string): T {
const layoutCapabilities: Record<LayoutCapability, boolean> = { broadcast: false, digital: false };
let activeRigId: string | null = null;
// A layout seeds the collapsible sections; it does not hold them there.
// applyLayout runs on every rig-state refresh, so re-applying the disclosure
// state unconditionally would reopen or close sections under the operator
// once a second — the reason a section could not be expanded while a layout
// that collapses it was selected. Sections are therefore written only when
// the layout actually changes, or the first time each one appears in the DOM
// (the advanced controls are built after the first applyLayout call).
const layoutSections: { id: string; key: "advanced" | "audio" | "scheduler" }[] = [
{ id: "advanced-radio-controls", key: "advanced" },
{ id: "audio-controls", key: "audio" },
{ id: "scheduler-controls", key: "scheduler" },
];
const seededSections = new Set<string>();
let appliedLayoutName: LayoutName | null = null;
function seedLayoutSections(layout: OperatorLayout, layoutChanged: boolean) {
layoutSections.forEach(({ id, key }) => {
const section = document.getElementById(id) as HTMLDetailsElement | null;
if (!section) return;
if (!layoutChanged && seededSections.has(id)) return;
seededSections.add(id);
section.open = layout[key];
});
}
function layoutStorageKey() {
return activeRigId ? `trxOperatorLayout:${activeRigId}` : "trxOperatorLayout";
}
@@ -255,12 +280,9 @@ function elementById<T extends HTMLElement>(id: string): T {
const layout = layouts[permittedName];
document.body.dataset.operatorLayout = permittedName in layouts ? permittedName : "compact";
if (options.persist !== false) localStorage.setItem(layoutStorageKey(), permittedName);
const details = document.getElementById("advanced-radio-controls") as HTMLDetailsElement | null;
if (details) details.open = layout.advanced;
const audioDetails = document.getElementById("audio-controls") as HTMLDetailsElement | null;
if (audioDetails) audioDetails.open = layout.audio;
const schedulerDetails = document.getElementById("scheduler-controls") as HTMLDetailsElement | null;
if (schedulerDetails) schedulerDetails.open = layout.scheduler;
const layoutChanged = appliedLayoutName !== permittedName;
appliedLayoutName = permittedName;
seedLayoutSections(layout, layoutChanged);
if (options.navigate && typeof browserWindow.navigateToTab === "function") {
browserWindow.navigateToTab(layout.preferredTab);
}
@@ -143,4 +143,31 @@ ui.applyLayout("digital");
assert.equal(document.body.dataset.operatorLayout, "compact");
assert.equal(localStorage.getItem("trxOperatorLayout:rig-b"), "compact");
// A layout seeds the collapsible sections but must not hold them there:
// applyLayout runs on every rig-state refresh, so an operator who expands a
// section a layout collapses by default had it shut again about once a second.
const schedulerSection = new Element("details", document);
schedulerSection.id = "scheduler-controls";
const audioSection = new Element("details", document);
audioSection.id = "audio-controls";
ui.setActiveRig("rig-c");
ui.applyLayout("compact");
assert.equal(schedulerSection.open, false, "compact seeds the scheduler section closed");
schedulerSection.open = true; // operator expands it
ui.applyLayout("compact"); // the next rig-state refresh
assert.equal(schedulerSection.open, true, "re-applying the same layout leaves sections alone");
ui.setActiveRig("rig-c");
assert.equal(schedulerSection.open, true, "a rig-state refresh leaves sections alone");
// Choosing a different layout is an explicit request for its defaults.
ui.applyLayout("full");
assert.equal(schedulerSection.open, true, "full seeds the scheduler section open");
assert.equal(audioSection.open, true, "full seeds the audio section open");
audioSection.open = false;
ui.applyLayout("compact");
assert.equal(audioSection.open, false, "compact seeds the audio section closed");
assert.equal(schedulerSection.open, false, "switching layout reseeds every section");
console.log("ui-core component tests passed");