Fix welcome dashboard jitter and refine Morse mode UI

Fix "What's New" section shifting up/down on smaller screens (#157) by
isolating the logo pulse animation to its own compositing layer, stabilizing
the scrollbar gutter, and pinning the welcome container dimensions.

Morse mode improvements: relocate scope and decoded output panels to the
main content area, use shared SDR device controls, and reduce panel heights
for better layout.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-02-25 23:26:47 +00:00
parent 8a46293e5c
commit dc7c05b03f
5 changed files with 97 additions and 58 deletions
+4
View File
@@ -206,6 +206,8 @@ body {
max-width: 900px;
z-index: 1;
animation: welcomeFadeIn 0.8s ease-out;
max-height: calc(100vh - 40px);
overflow: hidden;
}
@keyframes welcomeFadeIn {
@@ -232,6 +234,7 @@ body {
.welcome-logo {
animation: logoPulse 3s ease-in-out infinite;
will-change: filter;
}
@keyframes logoPulse {
@@ -332,6 +335,7 @@ body {
padding: 20px;
max-height: calc(100vh - 300px);
overflow-y: auto;
scrollbar-gutter: stable;
}
.changelog-release {
+3 -12
View File
@@ -11,7 +11,7 @@
.morse-scope-container canvas {
width: 100%;
height: 120px;
height: 80px;
display: block;
border-radius: 4px;
}
@@ -21,8 +21,8 @@
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 16px;
min-height: 200px;
padding: 12px;
min-height: 120px;
max-height: 400px;
overflow-y: auto;
font-family: var(--font-mono);
@@ -30,7 +30,6 @@
line-height: 1.6;
color: var(--text-primary);
word-wrap: break-word;
flex: 1;
}
.morse-decoded-panel:empty::before {
@@ -112,14 +111,6 @@
gap: 4px;
}
/* Visuals container layout */
#morseVisuals {
flex-direction: column;
gap: 12px;
padding: 16px;
height: 100%;
}
/* Word space styling */
.morse-word-space {
display: inline;
+28 -7
View File
@@ -67,11 +67,11 @@ var MorseMode = (function () {
frequency: document.getElementById('morseFrequency').value || '14.060',
gain: document.getElementById('morseGain').value || '0',
ppm: document.getElementById('morsePPM').value || '0',
device: document.getElementById('morseDevice').value || '0',
sdr_type: document.getElementById('morseSdrType').value || 'rtlsdr',
device: document.getElementById('deviceSelect')?.value || '0',
sdr_type: document.getElementById('sdrTypeSelect')?.value || 'rtlsdr',
tone_freq: document.getElementById('morseToneFreq').value || '700',
wpm: document.getElementById('morseWpm').value || '15',
bias_t: document.getElementById('morseBiasT').checked,
bias_t: typeof getBiasTEnabled === 'function' ? getBiasTEnabled() : false,
};
fetch('/morse/start', {
@@ -191,6 +191,8 @@ var MorseMode = (function () {
// Update count
var countEl = document.getElementById('morseCharCount');
if (countEl) countEl.textContent = state.charCount + ' chars';
var barChars = document.getElementById('morseStatusBarChars');
if (barChars) barChars.textContent = state.charCount + ' chars decoded';
}
function appendSpace() {
@@ -210,6 +212,8 @@ var MorseMode = (function () {
state.decodedLog = [];
var countEl = document.getElementById('morseCharCount');
if (countEl) countEl.textContent = '0 chars';
var barChars = document.getElementById('morseStatusBarChars');
if (barChars) barChars.textContent = '0 chars decoded';
}
// ---- Scope canvas ----
@@ -221,21 +225,28 @@ var MorseMode = (function () {
var dpr = window.devicePixelRatio || 1;
var rect = canvas.getBoundingClientRect();
canvas.width = rect.width * dpr;
canvas.height = 120 * dpr;
canvas.style.height = '120px';
canvas.height = 80 * dpr;
canvas.style.height = '80px';
scopeCtx = canvas.getContext('2d');
scopeCtx.scale(dpr, dpr);
scopeHistory = [];
var toneLabel = document.getElementById('morseScopeToneLabel');
var threshLabel = document.getElementById('morseScopeThreshLabel');
function draw() {
if (!scopeCtx) return;
var w = rect.width;
var h = 120;
var h = 80;
scopeCtx.fillStyle = '#0a0e14';
scopeCtx.fillStyle = '#050510';
scopeCtx.fillRect(0, 0, w, h);
// Update header labels
if (toneLabel) toneLabel.textContent = scopeToneOn ? 'ON' : '--';
if (threshLabel) threshLabel.textContent = scopeThreshold > 0 ? Math.round(scopeThreshold) : '--';
if (scopeHistory.length === 0) {
scopeAnim = requestAnimationFrame(draw);
return;
@@ -356,6 +367,16 @@ var MorseMode = (function () {
if (statusText) {
statusText.textContent = running ? 'Listening' : 'Standby';
}
// Toggle scope and output panels (pager/sensor pattern)
var scopePanel = document.getElementById('morseScopePanel');
var outputPanel = document.getElementById('morseOutputPanel');
if (scopePanel) scopePanel.style.display = running ? 'block' : 'none';
if (outputPanel) outputPanel.style.display = running ? 'block' : 'none';
var scopeStatus = document.getElementById('morseScopeStatusLabel');
if (scopeStatus) scopeStatus.textContent = running ? 'ACTIVE' : 'IDLE';
if (scopeStatus) scopeStatus.style.color = running ? '#0f0' : '#444';
}
function setFreq(mhz) {
+62 -21
View File
@@ -3008,24 +3008,6 @@
</div>
</div>
<!-- Morse Code Decoder Visuals -->
<div id="morseVisuals" style="display: none; flex-direction: column; gap: 12px; padding: 16px; height: 100%;">
<div class="morse-toolbar">
<button class="btn btn-sm btn-outline" onclick="MorseMode.exportTxt()">Export TXT</button>
<button class="btn btn-sm btn-outline" onclick="MorseMode.exportCsv()">Export CSV</button>
<button class="btn btn-sm btn-outline" id="morseCopyBtn" onclick="MorseMode.copyToClipboard()">Copy</button>
<button class="btn btn-sm btn-outline" onclick="MorseMode.clearText()">Clear</button>
</div>
<div class="morse-scope-container">
<canvas id="morseScopeCanvas"></canvas>
</div>
<div id="morseDecodedText" class="morse-decoded-panel"></div>
<div class="morse-status-bar">
<span class="status-item" id="morseStatusBarWpm">15 WPM</span>
<span class="status-item" id="morseStatusBarTone">700 Hz</span>
<span class="status-item" id="morseStatusBarChars">0 chars decoded</span>
</div>
</div>
<!-- Device Intelligence Dashboard (above waterfall for prominence) -->
<div class="recon-panel collapsed" id="reconPanel">
@@ -3082,6 +3064,42 @@
<div id="sensorTimelineContainer" style="display: none; margin-bottom: 12px;"></div>
<!-- Morse Signal Scope -->
<div id="morseScopePanel" style="display: none; margin-bottom: 12px;">
<div style="background: #0a0a0a; border: 1px solid #1a2e1a; border-radius: 6px; padding: 8px 10px; font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; font-size: 10px; color: #555; text-transform: uppercase; letter-spacing: 1px;">
<span>CW Tone Detection</span>
<div style="display: flex; gap: 14px;">
<span>TONE: <span id="morseScopeToneLabel" style="color: #0f0; font-variant-numeric: tabular-nums;">--</span></span>
<span>THRESH: <span id="morseScopeThreshLabel" style="color: #fa0; font-variant-numeric: tabular-nums;">--</span></span>
<span id="morseScopeStatusLabel" style="color: #444;">IDLE</span>
</div>
</div>
<canvas id="morseScopeCanvas" style="width: 100%; height: 80px; display: block; border-radius: 3px; background: #050510;"></canvas>
</div>
</div>
<!-- Morse Decoded Output -->
<div id="morseOutputPanel" style="display: none; margin-bottom: 12px;">
<div style="background: #0a0a0a; border: 1px solid #1a2e1a; border-radius: 6px; padding: 8px 10px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; font-size: 10px; color: #555; text-transform: uppercase; letter-spacing: 1px;">
<span>Decoded Text</span>
<div style="display: flex; gap: 6px;">
<button class="btn btn-sm btn-ghost" onclick="MorseMode.exportTxt()">TXT</button>
<button class="btn btn-sm btn-ghost" onclick="MorseMode.exportCsv()">CSV</button>
<button class="btn btn-sm btn-ghost" id="morseCopyBtn" onclick="MorseMode.copyToClipboard()">Copy</button>
<button class="btn btn-sm btn-ghost" onclick="MorseMode.clearText()">Clear</button>
</div>
</div>
<div id="morseDecodedText" class="morse-decoded-panel"></div>
<div class="morse-status-bar">
<span class="status-item" id="morseStatusBarWpm">15 WPM</span>
<span class="status-item" id="morseStatusBarTone">700 Hz</span>
<span class="status-item" id="morseStatusBarChars">0 chars decoded</span>
</div>
</div>
</div>
<div class="output-content signal-feed" id="output">
<div class="placeholder signal-empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
@@ -4064,6 +4082,7 @@
document.getElementById('subghzMode')?.classList.toggle('active', mode === 'subghz');
document.getElementById('spaceWeatherMode')?.classList.toggle('active', mode === 'spaceweather');
document.getElementById('waterfallMode')?.classList.toggle('active', mode === 'waterfall');
document.getElementById('morseMode')?.classList.toggle('active', mode === 'morse');
const pagerStats = document.getElementById('pagerStats');
@@ -4105,7 +4124,6 @@
const wefaxVisuals = document.getElementById('wefaxVisuals');
const spaceWeatherVisuals = document.getElementById('spaceWeatherVisuals');
const waterfallVisuals = document.getElementById('waterfallVisuals');
const morseVisuals = document.getElementById('morseVisuals');
if (wifiLayoutContainer) wifiLayoutContainer.style.display = mode === 'wifi' ? 'flex' : 'none';
if (btLayoutContainer) btLayoutContainer.style.display = mode === 'bluetooth' ? 'flex' : 'none';
if (satelliteVisuals) satelliteVisuals.style.display = mode === 'satellite' ? 'block' : 'none';
@@ -4123,7 +4141,6 @@
if (wefaxVisuals) wefaxVisuals.style.display = mode === 'wefax' ? 'flex' : 'none';
if (spaceWeatherVisuals) spaceWeatherVisuals.style.display = mode === 'spaceweather' ? 'flex' : 'none';
if (waterfallVisuals) waterfallVisuals.style.display = mode === 'waterfall' ? 'flex' : 'none';
if (morseVisuals) morseVisuals.style.display = mode === 'morse' ? 'flex' : 'none';
// Prevent Leaflet heatmap redraws on hidden BT Locate map containers.
if (typeof BtLocate !== 'undefined' && BtLocate.setActiveMode) {
@@ -4145,6 +4162,10 @@
const sensorTimelineContainer = document.getElementById('sensorTimelineContainer');
if (pagerTimelineContainer) pagerTimelineContainer.style.display = mode === 'pager' ? 'block' : 'none';
if (sensorTimelineContainer) sensorTimelineContainer.style.display = mode === 'sensor' ? 'block' : 'none';
const morseScopePanel = document.getElementById('morseScopePanel');
const morseOutputPanel = document.getElementById('morseOutputPanel');
if (morseScopePanel && mode !== 'morse') morseScopePanel.style.display = 'none';
if (morseOutputPanel && mode !== 'morse') morseOutputPanel.style.display = 'none';
// Update output panel title based on mode
const outputTitle = document.getElementById('outputTitle');
@@ -4198,7 +4219,27 @@
// Show RTL-SDR device section for modes that use it
const rtlDeviceSection = document.getElementById('rtlDeviceSection');
if (rtlDeviceSection) rtlDeviceSection.style.display = (mode === 'pager' || mode === 'sensor' || mode === 'rtlamr' || mode === 'aprs' || mode === 'sstv' || mode === 'weathersat' || mode === 'sstv_general' || mode === 'wefax') ? 'block' : 'none';
if (rtlDeviceSection) {
rtlDeviceSection.style.display = (mode === 'pager' || mode === 'sensor' || mode === 'rtlamr' || mode === 'aprs' || mode === 'sstv' || mode === 'weathersat' || mode === 'sstv_general' || mode === 'wefax' || mode === 'morse') ? 'block' : 'none';
// Save original sidebar position of SDR device section (once)
if (!rtlDeviceSection._origParent) {
rtlDeviceSection._origParent = rtlDeviceSection.parentNode;
rtlDeviceSection._origNext = rtlDeviceSection.nextElementSibling;
}
// For morse mode, move SDR device section inside the morse panel after the title
const morsePanel = document.getElementById('morseMode');
if (mode === 'morse' && morsePanel) {
const firstSection = morsePanel.querySelector('.section');
if (firstSection) firstSection.after(rtlDeviceSection);
} else if (rtlDeviceSection._origParent && rtlDeviceSection.parentNode !== rtlDeviceSection._origParent) {
// Restore to original sidebar position when leaving morse mode
if (rtlDeviceSection._origNext) {
rtlDeviceSection._origNext.before(rtlDeviceSection);
} else {
rtlDeviceSection._origParent.appendChild(rtlDeviceSection);
}
}
}
// Toggle mode-specific tool status displays
const toolStatusPager = document.getElementById('toolStatusPager');
-18
View File
@@ -39,24 +39,6 @@
<label>PPM Correction</label>
<input type="number" id="morsePPM" value="0" step="1" min="-100" max="100">
</div>
<div class="form-group">
<label>SDR Type</label>
<select id="morseSdrType">
<option value="rtlsdr" selected>RTL-SDR</option>
<option value="hackrf">HackRF</option>
<option value="limesdr">LimeSDR</option>
<option value="airspy">Airspy</option>
<option value="sdrplay">SDRPlay</option>
</select>
</div>
<div class="form-group">
<label>Device Index</label>
<input type="number" id="morseDevice" value="0" step="1" min="0" max="9">
</div>
<div class="form-group" style="display: flex; align-items: center; gap: 8px;">
<input type="checkbox" id="morseBiasT">
<label for="morseBiasT" style="margin: 0; cursor: pointer;">Bias-T Power</label>
</div>
</div>
<div class="section">