diff --git a/server/src/ledgrab/static/css/layout.css b/server/src/ledgrab/static/css/layout.css index 2ee0bc4..9358448 100644 --- a/server/src/ledgrab/static/css/layout.css +++ b/server/src/ledgrab/static/css/layout.css @@ -151,8 +151,6 @@ h1 { font-family: var(--font-mono, monospace); font-size: 0.7rem; color: var(--lux-ink-dim, var(--text-secondary)); - min-width: 0; - overflow: hidden; } .transport-status { diff --git a/server/src/ledgrab/static/css/modal.css b/server/src/ledgrab/static/css/modal.css index 2029096..47fbefe 100644 --- a/server/src/ledgrab/static/css/modal.css +++ b/server/src/ledgrab/static/css/modal.css @@ -2189,6 +2189,40 @@ box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary-color) 22%, transparent); } +/* Compact action button slotted into a .label-row alongside .hint-toggle. + Pushes itself to the trailing edge so the label and hint stay grouped + left while utility actions (e.g. refresh) sit at the row end. */ +.label-row-action { + margin-left: auto; + flex-shrink: 0; + background: none; + border: 1px solid var(--border-color); + border-radius: 50%; + width: 20px; + height: 20px; + font-size: 0.85rem; + line-height: 1; + color: var(--text-secondary); + cursor: pointer; + padding: 0; + display: inline-flex; + align-items: center; + justify-content: center; + opacity: 0.7; + transition: opacity 0.2s, color 0.2s, border-color 0.2s; +} + +.label-row-action:hover { + opacity: 1; + color: var(--lux-ink, var(--text-color)); + border-color: var(--lux-line-bold, var(--border-color)); +} + +.label-row-action:disabled { + opacity: 0.35; + cursor: not-allowed; +} + /* Floating hint tooltip — anchored to the active `.hint-toggle` button. Replaces the legacy inline small.input-hint reveal which pushed the form below it down on every help click. The popover is `position: @@ -2406,16 +2440,6 @@ min-width: auto; } -.select-with-action { - display: flex; - gap: 6px; - align-items: center; -} - -.select-with-action select { - flex: 1; - min-width: 0; -} .fps-hint { display: block; diff --git a/server/src/ledgrab/static/js/features/audio-sources.ts b/server/src/ledgrab/static/js/features/audio-sources.ts index 41dbc2f..4c06ec2 100644 --- a/server/src/ledgrab/static/js/features/audio-sources.ts +++ b/server/src/ledgrab/static/js/features/audio-sources.ts @@ -279,8 +279,15 @@ function _filterDevicesBySelectedTemplate() { const select = document.getElementById('audio-source-device') as HTMLSelectElement | null; if (!select) return; + // Snapshot current selection BEFORE rebuilding options. We try to + // restore it afterwards by: + // 1. exact value match — `${index}:${loopback}` — survives a refresh + // whenever the OS keeps the same device index (the common case); + // 2. name match — survives an OS-side reindex (Windows occasionally + // reorders devices) provided the device label is unchanged. + const prevValue = select.value; const prevOption = select.options[select.selectedIndex]; - const prevName = prevOption ? prevOption.textContent : ''; + const prevName = (prevOption?.textContent ?? '').trim(); const templateId = ((document.getElementById('audio-source-audio-template') as HTMLSelectElement | null) || { value: '' } as any).value; const templates = _cachedAudioTemplates || []; @@ -305,9 +312,16 @@ function _filterDevicesBySelectedTemplate() { select.innerHTML = ''; } - if (prevName) { - const match = Array.from(select.options).find((o: HTMLOptionElement) => o.textContent === prevName); - if (match) select.value = match.value; + const opts = Array.from(select.options) as HTMLOptionElement[]; + let restored: HTMLOptionElement | undefined; + if (prevValue) { + restored = opts.find(o => o.value === prevValue); + } + if (!restored && prevName) { + restored = opts.find(o => (o.textContent ?? '').trim() === prevName); + } + if (restored) { + select.value = restored.value; } if (_asDeviceEntitySelect) _asDeviceEntitySelect.destroy(); @@ -330,7 +344,14 @@ function _selectAudioDevice(deviceIndex: any, isLoopback: any) { if (!select) return; const val = `${deviceIndex ?? -1}:${isLoopback !== false ? '1' : '0'}`; const opt = Array.from(select.options).find((o: HTMLOptionElement) => o.value === val); - if (opt) select.value = val; + if (opt) { + select.value = val; + // EntitySelect's trigger button is a separate DOM node populated at + // construction time from the select's then-current value. Without + // this, the trigger keeps showing the first option even though the + // native select already points at the saved device. + if (_asDeviceEntitySelect) _asDeviceEntitySelect.setValue(val); + } } function _loadParentSources(selectedId?: any) { diff --git a/server/src/ledgrab/templates/modals/audio-source-editor.html b/server/src/ledgrab/templates/modals/audio-source-editor.html index 59e875a..fc9adc8 100644 --- a/server/src/ledgrab/templates/modals/audio-source-editor.html +++ b/server/src/ledgrab/templates/modals/audio-source-editor.html @@ -64,14 +64,12 @@